| name | club-3090-llm-serving |
| description | Recipes and configs for serving LLMs locally on RTX 3090 GPUs using vLLM, llama.cpp, and SGLang with OpenAI-compatible API |
| triggers | ["serve LLM on RTX 3090","run Qwen on 3090","local LLM serving with vLLM","llama.cpp 262K context single card","dual 3090 inference setup","club-3090 config","local OpenAI-compatible API","homelab LLM docker compose"] |
club-3090 LLM Serving
Skill by ara.so — Daily 2026 Skills collection.
Community recipes for serving modern LLMs on RTX 3090 (24 GB) hardware. Supports vLLM, llama.cpp, and SGLang engines with validated Docker Compose configs exposing an OpenAI-compatible API on localhost:8020. Currently ships Qwen3.6-27B configs for 1× and 2× cards.
Engine Decision Matrix
| Need | Engine | Why |
|---|
| Max throughput (code/chat) | vLLM dual | 89–127 TPS, MTP n=3, vision, tools |
| Full 262K context, no crashes | llama.cpp single | No prefill cliffs, stable tool-use |
| 4 concurrent streams @ 262K | vLLM dual turbo | Stream isolation, full feature stack |
| Single card, moderate ctx | vLLM default | ~89 TPS, easiest setup |
SGLang is currently blocked on Qwen3.6-27B — see models/qwen3.6-27b/sglang/README.md.
Prerequisites
- 1× or 2× NVIDIA RTX 3090 (24 GB each)
- Linux (Ubuntu 22.04+ recommended)
- Docker + NVIDIA Container Toolkit
- NVIDIA driver 580.x+
- ~30 GB free disk per model
Installation & Setup
1. Clone the repo
git clone https://github.com/noonghunna/club-3090.git
cd club-3090
2. Download and verify a model
bash scripts/setup.sh qwen3.6-27b
3. Launch (interactive wizard)
bash scripts/launch.sh
4. Launch (non-interactive)
bash scripts/launch.sh --variant vllm/default
bash scripts/launch.sh --variant vllm/dual
bash scripts/launch.sh --variant llamacpp/default
bash scripts/switch.sh --list
Key Scripts
| Script | Purpose |
|---|
scripts/setup.sh <model> | Preflight checks, model download, SHA verify, Genesis patch clone |
scripts/launch.sh [--variant X] | Interactive or direct variant boot; calls switch.sh + verify-full.sh |
scripts/switch.sh <variant> | Stateless switcher — tears down old compose, brings up new one |
scripts/health.sh | Live health probe: KV %, MTP accept-length, recent TPS, errors |
scripts/verify.sh | Quick smoke test (engine-aware via env vars) |
scripts/verify-full.sh | 8-check functional test (~1–2 min) |
scripts/verify-stress.sh | Boundary stress test: 262K ladder + tool prefill OOM (~5–10 min) |
scripts/bench.sh | Canonical TPS benchmark (3 warm + 5 measured runs) |
Common script usage
bash scripts/switch.sh vllm/long-vision
bash scripts/switch.sh vllm/dual
bash scripts/switch.sh llamacpp/default
bash scripts/health.sh
bash scripts/bench.sh
bash scripts/verify-full.sh
bash scripts/verify-stress.sh
Variant Names Reference
vllm/default Single-card, chat-optimized (recommended first start)
vllm/dual Dual-card, 262K ctx, vision, tools, MTP n=3
vllm/long-vision Dual-card, long-context + vision workloads
vllm/turbo Dual-card, 4 concurrent streams @ 262K
llamacpp/default Single-card, full 262K, no prefill cliffs
llamacpp/65k Single-card, 65K ctx (faster, more VRAM headroom)
llamacpp/dual Dual-card llama.cpp recipe
API Usage (OpenAI-compatible, port 8020)
The server exposes a standard OpenAI-compatible API. Use the openai Python SDK pointed at localhost:8020.
Python — openai SDK
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:8020/v1",
api_key="ignored",
)
response = client.chat.completions.create(
model="qwen3.6-27b-autoround",
messages=[{"role": "user", "content": "Explain KV cache in one paragraph."}],
max_tokens=512,
)
print(response.choices[0].message.content)
Python — streaming
stream = client.chat.completions.create(
model="qwen3.6-27b-autoround",
messages=[{"role": "user", "content": "Write a Python quicksort."}],
max_tokens=1024,
stream=True,
)
for chunk in stream:
delta = chunk.choices[0].delta.content
if delta:
print(delta, end="", flush=True)
print()
Python — raw requests (no SDK dependency)
import requests, json
payload = {
"model": "qwen3.6-27b-autoround",
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "What is the capital of France?"},
],
"max_tokens": 200,
"temperature": 0.7,
}
resp = requests.post(
"http://localhost:8020/v1/chat/completions",
headers={"Content-Type": "application/json"},
json=payload,
timeout=120,
)
resp.raise_for_status()
print(resp.json()["choices"][0]["message"]["content"])
Python — tool calling
tools = [
{
"type": "function",
"function": {
"name": "search_web",
"description": "Search the web for recent information",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search query"},
},
"required": ["query"],
},
},
}
]
response = client.chat.completions.create(
model="qwen3.6-27b-autoround",
messages=[{"role": "user", "content": "What's the latest news on CUDA 13?"}],
tools=tools,
tool_choice="auto",
max_tokens=512,
)
msg = response.choices[0].message
if msg.tool_calls:
for call in msg.tool_calls:
print(f"Tool: {call.function.name}")
print(f"Args: {call.function.arguments}")
Python — long context (262K, use with llamacpp/default or vllm/dual)
with open("large_codebase.txt") as f:
document = f.read()
response = client.chat.completions.create(
model="qwen3.6-27b-autoround",
messages=[
{"role": "user", "content": f"Summarize the architecture:\n\n{document}"},
],
max_tokens=1024,
)
print(response.choices[0].message.content)
TypeScript / Node
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "http://localhost:8020/v1",
apiKey: "ignored",
});
async function chat(prompt: string): Promise<string> {
const response = await client.chat.completions.create({
model: "qwen3.6-27b-autoround",
messages: [{ role: "user", content: prompt }],
max_tokens: 512,
});
return response.choices[0].message.content ?? "";
}
async function streamChat(prompt: string): Promise<void> {
const stream = await client.chat.completions.create({
model: "qwen3.6-27b-autoround",
messages: [{ role: "user", content: prompt }],
max_tokens: 1024,
stream: true,
});
for await (const chunk of stream) {
process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}
console.log();
}
curl — quick sanity check
curl -sf http://localhost:8020/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "qwen3.6-27b-autoround",
"messages": [{"role": "user", "content": "Capital of France?"}],
"max_tokens": 200
}' | jq '.choices[0].message.content'
curl — list available models
curl -sf http://localhost:8020/v1/models | jq '.data[].id'
Docker Compose Structure
Configs live under models/qwen3.6-27b/vllm/compose/. Example structure of a single-card compose:
services:
vllm:
image: vllm/vllm-openai:v0.20.1rc1.dev16+g7a1eb8ac2
runtime: nvidia
environment:
- NVIDIA_VISIBLE_DEVICES=0
- CUDA_VISIBLE_DEVICES=0
ports:
- "8020:8000"
volumes:
- ${MODEL_PATH}:/models/qwen3.6-27b
- ${PATCH_PATH}:/patches
command: >
--model /models/qwen3.6-27b
--served-model-name qwen3.6-27b-autoround
--tensor-parallel-size 1
--max-model-len 65536
--kv-cache-dtype fp8
--speculative-model /models/qwen3.6-27b/mtp_head
--num-speculative-tokens 3
--port 8000
For dual-card, tensor-parallel-size 2 and NVIDIA_VISIBLE_DEVICES=0,1 are set, and max-model-len extends to 262144.
Connecting External Clients
Open WebUI
API Base URL: http://localhost:8020/v1
API Key: (leave blank or type anything)
Model: qwen3.6-27b-autoround
Cline / Cursor / Copilot-compatible tools
{
"openai.baseURL": "http://localhost:8020/v1",
"openai.apiKey": "local",
"openai.model": "qwen3.6-27b-autoround"
}
LiteLLM proxy passthrough
model_list:
- model_name: qwen3.6-27b
litellm_params:
model: openai/qwen3.6-27b-autoround
api_base: http://localhost:8020/v1
api_key: ignored
Repo Layout Quick Reference
club-3090/
├── scripts/ Shared model-aware scripts (setup, launch, bench, health)
├── models/
│ └── qwen3.6-27b/
│ ├── vllm/
│ │ ├── compose/ Docker Compose files (all variants)
│ │ └── patches/ tolist_cudagraph, Marlin pad, Genesis pointer
│ ├── llama-cpp/
│ │ └── recipes/ Single-card 65K / 262K-max / dual recipes
│ └── sglang/ Blocked — watch list only
└── docs/
├── SINGLE_CARD.md 1× 3090 workload → config guide
├── DUAL_CARD.md 2× 3090 workload → config guide
├── HARDWARE.md PCIe vs NVLink, power draw, card compatibility
├── GLOSSARY.md TPS / KV / MTP / TP / prefill cliff definitions
├── CLIFFS.md Prefill cliff root causes and fix landscape
├── COMPARISONS.md Self-host vs cloud cost crossover analysis
├── UPSTREAM.md Tracked upstream issues and PRs
└── engines/ Per-engine deep dives (vLLM / llama.cpp / SGLang)
Troubleshooting
Server won't start — CUDA/driver error
nvidia-smi --query-gpu=driver_version --format=csv,noheader
docker run --rm --gpus all nvidia/cuda:12.0-base nvidia-smi
nvidia-smi -L
Out of VRAM / OOM on prefill
bash scripts/health.sh
bash scripts/switch.sh llamacpp/65k
bash scripts/switch.sh llamacpp/default
Prefill cliff (vLLM hangs or errors on large prompts)
This is a known DeltaNet architecture issue on Qwen3.6-27B with vLLM. The llama.cpp route avoids it entirely:
bash scripts/switch.sh llamacpp/default
bash scripts/verify-stress.sh
For vLLM workarounds, see models/qwen3.6-27b/INTERNALS.md and docs/CLIFFS.md.
MTP / speculative decoding not accepting tokens
bash scripts/health.sh
bash scripts/setup.sh qwen3.6-27b
Tool call returns 25K+ tokens and hangs
Known failure mode on vLLM with very large tool responses. Use llama.cpp:
bash scripts/switch.sh llamacpp/default
Switching variants leaves old container running
docker compose -f models/qwen3.6-27b/vllm/compose/default.yml down
bash scripts/switch.sh vllm/dual
Check what variant is currently running
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Ports}}"
Performance Reference (Qwen3.6-27B)
| Config | Cards | TPS (narrative) | TPS (code) | Max ctx | Notes |
|---|
vllm/default | 1× | ~89 | ~89 | 65K | Recommended starting point |
vllm/dual | 2× | ~89 | ~127 | 262K | DFlash on code workloads |
vllm/turbo | 2× | — | — | 262K | 4 concurrent streams |
llamacpp/default | 1× | ~21 | ~21 | 262K | No cliffs, stable tool-use |
Benchmark substrate: vLLM nightly 0.20.1rc1.dev16+g7a1eb8ac2 + Genesis v7.65 dev, llama.cpp 0d0764dfd, RTX 3090 sm_86 PCIe @ 230 W. Full per-run numbers in models/qwen3.6-27b/CHANGELOG.md.
Adding a New Model
mkdir -p models/glm-4.6/{vllm/compose,vllm/patches,llama-cpp/recipes,sglang}
bash scripts/setup.sh glm-4.6
Key Links