Consolidates and supersedes:
- noonghunna/qwen36-27b-single-3090
- noonghunna/qwen36-dual-3090
The two predecessor repos partitioned by card count (1× vs 2×). This
repo partitions by engine instead, which matches how users actually
decide ("vLLM or llama.cpp?" before "1 card or 2"). Card count becomes
a config variant within each engine.
Structure (model-agnostic from day 1):
docs/ cross-model engine + hardware docs
engines/ vLLM / llama.cpp / SGLang comparison + per-engine deep dives
HARDWARE.md Ampere SM 8.6+, NVLink, power, VRAM ceilings
GLOSSARY.md plain-language definitions
img/ illustrations (vram-budget.svg)
ARCHITECTURE.md how this stack thinks about LLM serving on 24 GB
models/<model-name>/ everything specific to a model
qwen3.6-27b/ today's only model
README.md / INTERNALS.md / USE_CASES.md / CHANGELOG.md
vllm/ vLLM-specific configs for this model
compose/ docker-compose files (single + dual variants)
patches/ tolist_cudagraph + Marlin pad notes
llama-cpp/ llama.cpp recipes for this model
recipes/ shell scripts (single-card default + 262K max-ctx)
sglang/ SGLang status (currently blocked)
scripts/ shared, model-aware
setup.sh bash setup.sh <model> → downloads + verifies
verify.sh / verify-full.sh smoke + functional tests
bench.sh canonical TPS bench
vLLM compose variants (all under models/qwen3.6-27b/vllm/compose/):
Single-card:
docker-compose.yml ⭐ DEFAULT — TQ3 + Genesis P65, 48K, 51/68 TPS
docker-compose.fast-chat.yml fp8 + 20K, 55/70 TPS — fastest at small ctx
docker-compose.tools-text.yml fp8 + 75K, 53/70 TPS — best for long single prompts
docker-compose.no-genesis-mtp.yml control variant
docker-compose.minimal.yml no spec-decode
Dual-card:
docker-compose.dual.yml ⭐ fp8 + 262K + MTP + vision, 71/89 TPS
docker-compose.dual-turbo.yml TQ3 + Genesis v7.14 — 4-stream concurrency
docker-compose.dual-dflash.yml DFlash N=5 + 185K + vision — 78/128 TPS
docker-compose.dual-dflash-noviz.yml DFlash + 200K text-only
llama.cpp recipes (under models/qwen3.6-27b/llama-cpp/recipes/):
single-card-default.sh Q4_K_M + 65K
single-card-max-ctx.sh Q4_K_M + q4_0 KV at full 262K — the standout recipe
Old repos remain readable for issue history + external links (Medium,
Reddit, Twitter, Sandermage's PR threads). New issues should be filed
here.
Credits in README. Apache 2.0.
156 lines
6.9 KiB
Bash
Executable File
156 lines
6.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Post-setup smoke test — confirms the stack is healthy before you start using it.
|
|
#
|
|
# Runs four checks, each short-circuits on failure with an actionable hint:
|
|
# 1. Server responds on /v1/models
|
|
# 2. Genesis patches applied cleanly (tool_call fix is the fragile one)
|
|
# 3. Basic text completion works (Paris sanity)
|
|
# 4. Tool calling works end-to-end (request includes tools → response has tool_calls[])
|
|
#
|
|
# If check 4 fails but checks 1-3 pass, your Genesis tool_call patch didn't apply
|
|
# and you're on a vLLM nightly that drifted past our pinned digest. See the README
|
|
# troubleshooting section.
|
|
#
|
|
# Env vars (optional):
|
|
# URL Override endpoint. Default: http://localhost:8020
|
|
# MODEL Served model name. Default: qwen3.6-27b-autoround
|
|
# CONTAINER Docker container name for log scraping. Default: vllm-qwen36-27b
|
|
|
|
set -euo pipefail
|
|
|
|
URL="${URL:-http://localhost:8020}"
|
|
MODEL="${MODEL:-qwen3.6-27b-autoround}"
|
|
CONTAINER="${CONTAINER:-vllm-qwen36-27b}"
|
|
|
|
pass() { printf " \033[32m✓\033[0m %s\n" "$1"; }
|
|
fail() { printf " \033[31m✗\033[0m %s\n" "$1"; printf " \033[33m→\033[0m %s\n" "$2"; exit 1; }
|
|
|
|
echo "Running smoke test against ${URL} (model=${MODEL}, container=${CONTAINER})"
|
|
echo ""
|
|
|
|
# --------------------------------------------------------------------
|
|
# 1. Server reachable
|
|
# --------------------------------------------------------------------
|
|
echo "[1/4] Server reachable on /v1/models ..."
|
|
if curl -sf -m 5 "${URL}/v1/models" >/dev/null 2>&1; then
|
|
pass "server is serving"
|
|
else
|
|
fail "no response from ${URL}/v1/models" \
|
|
"Start the stack: cd compose && docker compose up -d ; docker logs -f ${CONTAINER}"
|
|
fi
|
|
|
|
# --------------------------------------------------------------------
|
|
# 2. Genesis patches applied cleanly — especially the fragile tool_call one
|
|
# --------------------------------------------------------------------
|
|
echo "[2/4] Genesis patches applied (Qwen3 tool_call fix in particular) ..."
|
|
if ! command -v docker >/dev/null 2>&1; then
|
|
echo " (skipped — docker not in PATH, cannot read container logs)"
|
|
elif ! docker inspect "${CONTAINER}" >/dev/null 2>&1; then
|
|
echo " (skipped — container '${CONTAINER}' not found; if your container has a different name, set CONTAINER=...)"
|
|
else
|
|
logs="$(docker logs "${CONTAINER}" 2>&1 | grep -E "Qwen3 tool_call fix|\[FAILED\]" | tail -5)"
|
|
if echo "$logs" | grep -q "\[OK\] Qwen3 tool_call fix"; then
|
|
pass "Genesis Qwen3 tool_call fix applied"
|
|
elif echo "$logs" | grep -q "\[FAILED\] Qwen3 tool_call fix"; then
|
|
fail "Genesis Qwen3 tool_call fix [FAILED]" \
|
|
"Your vLLM image drifted past the patch anchor. Pin to sha256:9bba4628a3b9... in compose/docker-compose.yml (already pinned by default). Re-pull if you bumped it manually."
|
|
else
|
|
echo " (warn — no Genesis OK/FAILED marker for tool_call in logs; container may have been restarted. Continuing.)"
|
|
fi
|
|
fi
|
|
|
|
# --------------------------------------------------------------------
|
|
# 3. Basic completion — Paris sanity
|
|
# --------------------------------------------------------------------
|
|
echo "[3/4] Basic completion — capital of France ..."
|
|
resp="$(curl -sf -m 30 "${URL}/v1/chat/completions" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{
|
|
\"model\": \"${MODEL}\",
|
|
\"messages\": [{\"role\": \"user\", \"content\": \"What is the capital of France? Reply in one short sentence.\"}],
|
|
\"max_tokens\": 30,
|
|
\"temperature\": 0.6,
|
|
\"chat_template_kwargs\": {\"enable_thinking\": false}
|
|
}")" || fail "completion request failed" "Check docker logs ${CONTAINER}"
|
|
|
|
content="$(echo "$resp" | python3 -c "import sys,json; print(json.load(sys.stdin)['choices'][0]['message']['content'])" 2>/dev/null || true)"
|
|
if echo "$content" | grep -qi "Paris"; then
|
|
pass "reply contains 'Paris': $(echo "$content" | head -c 70)..."
|
|
else
|
|
fail "reply didn't mention Paris: $(echo "$content" | head -c 80)" \
|
|
"Model may be loading badly or using wrong chat template. Check docker logs ${CONTAINER}."
|
|
fi
|
|
|
|
# --------------------------------------------------------------------
|
|
# 4. Tool calling end-to-end — request with tools[] → response with tool_calls[]
|
|
# --------------------------------------------------------------------
|
|
echo "[4/4] Tool calling — model should populate tool_calls[] ..."
|
|
tool_resp="$(curl -sf -m 60 "${URL}/v1/chat/completions" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{
|
|
\"model\": \"${MODEL}\",
|
|
\"messages\": [
|
|
{\"role\": \"user\", \"content\": \"What is the weather in San Francisco right now? Use the get_weather tool.\"}
|
|
],
|
|
\"tools\": [
|
|
{
|
|
\"type\": \"function\",
|
|
\"function\": {
|
|
\"name\": \"get_weather\",
|
|
\"description\": \"Get the current weather for a given city.\",
|
|
\"parameters\": {
|
|
\"type\": \"object\",
|
|
\"properties\": {
|
|
\"city\": {\"type\": \"string\", \"description\": \"City name\"},
|
|
\"units\": {\"type\": \"string\", \"enum\": [\"celsius\", \"fahrenheit\"]}
|
|
},
|
|
\"required\": [\"city\"]
|
|
}
|
|
}
|
|
}
|
|
],
|
|
\"tool_choice\": \"auto\",
|
|
\"max_tokens\": 200,
|
|
\"temperature\": 0.3,
|
|
\"chat_template_kwargs\": {\"enable_thinking\": false}
|
|
}")" || fail "tool-call request failed" "Check docker logs ${CONTAINER}"
|
|
|
|
tool_calls="$(echo "$tool_resp" | python3 -c "
|
|
import sys, json
|
|
try:
|
|
d = json.load(sys.stdin)
|
|
tc = d['choices'][0]['message'].get('tool_calls')
|
|
if tc:
|
|
print(json.dumps(tc, indent=2))
|
|
else:
|
|
# Check if model inlined <tool_call> as plain text — the symptom of a broken patch
|
|
content = d['choices'][0]['message'].get('content') or ''
|
|
if '<tool_call>' in content:
|
|
print('__INLINED__', content[:200], sep='\n')
|
|
else:
|
|
print('__NONE__', content[:200], sep='\n')
|
|
except Exception as e:
|
|
print(f'__PARSE_ERROR__: {e}')
|
|
" 2>&1)"
|
|
|
|
if echo "$tool_calls" | grep -q "__INLINED__"; then
|
|
fail "model emitted <tool_call> as inline text (tool_calls[] is empty)" \
|
|
"Genesis Patch 12 (Qwen3 tool_call fix) did not apply. Re-check the container logs and pin the image digest. README § Troubleshooting has the full chain."
|
|
elif echo "$tool_calls" | grep -q "__NONE__"; then
|
|
fail "model answered without invoking the tool" \
|
|
"May be a model-behavior issue (it chose not to call) rather than a patch issue. Try rephrasing the prompt or lowering temperature. Raw content: $(echo "$tool_calls" | tail -1)"
|
|
elif echo "$tool_calls" | grep -q "__PARSE_ERROR__"; then
|
|
fail "couldn't parse the response JSON" \
|
|
"Response was: $(echo "$tool_resp" | head -c 400)"
|
|
elif echo "$tool_calls" | grep -qi "get_weather"; then
|
|
pass "tool_calls[] populated, includes get_weather:"
|
|
echo "$tool_calls" | head -20 | sed 's/^/ /'
|
|
else
|
|
fail "unexpected tool_calls structure" \
|
|
"Raw: $(echo "$tool_calls" | head -c 300)"
|
|
fi
|
|
|
|
echo ""
|
|
echo "All checks passed. Stack is ready for use."
|