C++20 local inference engine for the Qwen3.6-27B and Qwen3.8-27B hybrid architecture.
This is not a wrapper around llama.cpp, ggml, vLLM, or MLC.
| Engine license | MIT (LICENSE) |
| Model weights | Apache-2.0 (Qwen) |
| Language | C++20 (Python only for goldens / fixtures) |
| Platforms | Windows x86-64 · Linux x86-64 |
| Version | 0.1.0 |
Supported now
| Architecture | Qwen3.6-27B and Qwen3.8-27B (qwen3_5 hybrid: 48 Gated DeltaNet + 16 Gated Attention) |
| Speculative decode | --spec off|ngram|mtp|auto |
| MTP | Target model's own multi-token prediction head (mtp.fc / mtp.norm) as draft |
| Continuous batch | --batch N / rapidllm_generate_batch — one shared weight pass per step |
| vs vLLM (same box) | Short-decode 1.50–1.80×; hybrid GGUF; 163k–262k alloc. See Conclusion vs vLLM |
Design: docs/architecture.md · docs/设计方案.md
RapidLLM loads official HuggingFace block-FP8 directories and community GGUF files into one ModelDesc + TensorTable. Qwen3.6-27B and Qwen3.8-27B share this IR. The scheduler walks layer_types[] and dispatches:
- 48 × Gated DeltaNet (
linear_attention) — O(1) FP32 recurrent state - 16 × Gated Attention (
full_attention) — GQA KV cache
The default path is text-only. Vision tensors are skipped unless --vision or --image is set. MTP is loaded and used for speculative decode (--spec mtp / auto).
Short decode is still a weight-bandwidth problem (22–30 GB of linears). Long context is where the architecture wins:
| State | Size | Grows with sequence? |
|---|---|---|
| Gated Attention KV (16 layers, FP16) | 64 KiB / token | yes |
Gated Attention KV (--kv-type q8k_tq3v) |
~22 KiB / token | yes (q8 K + TurboQuant-3 V) |
DeltaNet recurrent S (48 layers, FP32) |
~144 MiB | no |
| conv1d window | ~7.5 MiB | no |
At 32K context, KV is about 2 GiB FP16. --ctx>163840 (or --kv-type q8k_tq3v / RAPIDLLM_KV_TQ=1) keeps K as q8 and compresses V with a Walsh–Hadamard + 3-bit Lloyd-Max codebook so a 48 GB card can allocate 262144. Compute for prefixes that fit the F16 window (8k) stays on the existing F16 attn path; T=1 FP8 GEMV is unchanged. RAPIDLLM_KV_TQ=0 forces a full F16 cache (will OOM at 262k on 48 GB). The 48 linear-attention layers do not grow a KV cache.
- First-class HF FP8 loader (
config.json+ safetensors / index) - First-class GGUF loader (Q4_K / Q5_K / Q6_K / Q8_0; unknown IQ dequantized once)
- CPU decode by default: AVX2 required, AVX-512 dispatched at runtime
- Optional CUDA (
-DRAPIDLLM_WITH_CUDA=ON) with host-ref kernels when no device is present - Dual cache: attention KV + DeltaNet recurrent + causal conv state
- Optional TurboQuant-style KV (
--kv-type q8k_tq3v): q8 keys + 3-bit WHT values, ~3× smaller than FP16 - Memory planner: refuse or shrink context instead of letting the OS OOM-kill the process
- Fused decode path (
--fuse=on|off) for A/B against unfused ops - Speculative decode:
off/ngram/mtp/auto - MTP: 27B's embedded 1-layer MTP head drafts tokens;
--spec mtpor defaultauto - Continuous batch:
--batch N(same prompt, one shared weight pass per step; CUDA viaRAPIDLLM_MAX_BATCH) - HTTP serve: OpenAI
/v1/chat/completions+/v1/responses, Anthropic/v1/messages - Qwen3.5 / 3.6 / 3.8 ViT + CLI image+text generate (
--image PATH) - Thinking on by default;
benchandserveforceenable_thinking=false - Versioned C API (
include/rapidllm/api.h) + CLI
- CMake ≥ 3.20
- C++20 compiler (MSVC, Clang-cl, or GCC/Clang)
- Python 3 (fixture / golden generation only)
- x86-64 with AVX2
- Optional: CUDA Toolkit (sm_75 / sm_86 / sm_89)
RAM guide
| Machine RAM | Recommended weights | Default ctx |
|---|---|---|
| ≤ 16 GB | tiny fixture only | — |
| 32 GB | GGUF Q4_K (~16.8 GB) | 32K, INT8 KV |
| 64 GB | official FP8 or Q4_K / Q5_K | 32K |
| ≥ 96 GB | FP8; 262K is the capability cap | 32K–128K |
Factory default ctx = 32768. Official FP8 text weights are ~27–28 GiB resident — a 32 GB box must use GGUF Q4_K or INT4 repack. Loading the vision tower adds another ~1–2 GiB.
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build buildWith NVIDIA kernels:
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DRAPIDLLM_WITH_CUDA=ON
cmake --build buildOn Windows without Ninja, omit -G Ninja and use the Visual Studio generator. rapidllm serve links ws2_32 on Windows.
rapidllm -m <hf-dir|file.gguf> [--device cpu|cuda|vulkan] [--ctx 32768] [--prompt TEXT]
[--max-new N] [--threads N] [--max-layers N] [--max-ram-mb N]
[--thinking | --no-thinking] [--fuse=on|off]
[--spec off|ngram|mtp|auto] [--spec-n N]
[--image PATH] [--vision] [--batch N]
rapidllm bench -m <path> [--device cpu|cuda|vulkan] [--fuse=on|off] [--micro] [--batch N]
rapidllm serve -m <path> [--host 127.0.0.1] [--port 8080] [--device cpu|cuda|vulkan]
Examples:
rapidllm -m /path/to/Qwen3.6-27B-FP8 --device cpu --ctx 32768 --prompt "Hello"
rapidllm -m model.gguf --device cpu --prompt "Hello"
rapidllm -m /path/to/Qwen3.6-27B-FP8 --spec mtp --spec-n 3 --prompt "Hello"
rapidllm bench -m model.gguf
rapidllm bench -m model.gguf --device cuda --batch 4
rapidllm bench -m /path/to/Qwen3.6-27B-FP8 --device cuda --ctx 131072 --prompt-n 8192 --max-new 8 --spec off
rapidllm bench --micro
rapidllm serve -m /path/to/Qwen3.6-27B-FP8 --host 127.0.0.1 --port 8080 --device cuda
rapidllm -m /path/to/Qwen3.8-27B-FP8 --image photo.png --prompt "What is in this image?" --max-new 64 --device cuda| Flag | Default | Notes |
|---|---|---|
--device |
cpu |
cuda requires a CUDA build |
--ctx |
32768 |
planner may shrink or refuse |
--max-new |
8 |
generated tokens |
--fuse |
on |
fused DeltaNet / Attn / MLP decode |
--spec |
auto |
mtp if the checkpoint has an MTP head, else ngram |
--kv-type |
f16; auto q8k_tq3v if --ctx>163840 |
q8k_tq3v = K q8 + V TurboQuant-3. Env RAPIDLLM_KV_TQ=0|1 |
--batch |
1 |
concurrent sequences (sets RAPIDLLM_MAX_BATCH, cap 128). Add --mixed for independent prompts |
--prompt-n |
— | synthesize N non-repeating token ids (skips the tokenizer). For long-ctx benches. |
--vision / --image |
off | load visual.*. --image PATH runs the ViT and splices visual tokens into generate |
--thinking |
on | bench and serve always turn it off |
Weight sources:
- Target (latest):
Qwen/Qwen3.8-27B-FP8— sameqwen3_5hybrid IR as 3.6-27B - Target (previous):
Qwen/Qwen3.6-27B-FP8 - Community GGUF:
unsloth/Qwen3.8-27B-GGUFQ4_K_M / Q5_K_M / Q6_K / Q8_0 (alsounsloth/Qwen3.6-27B-GGUF)
Same official pair as 3.6: prompt 1,2,3, 16 new, --spec off --fuse=on --device cuda --ctx 256, thinking off. Wall tok/s after warmup generate. Q4_K / Q5_K / Q6_K stay packed and use native CUDA GEMV (set RAPIDLLM_REQUANT_KQUANT=1 to requant to FP8). Q8_0 stays packed and uses the native Q8 GEMV.
| Weights | Size | Wall tok/s | Decode tok/s | Prefill s | Notes |
|---|---|---|---|---|---|
| Official FP8 | 30.9 GB | 28.99 | 30.72 | 0.064 | T=1 FP8 GEMV (repeat 28.99) |
| Unsloth Q4_K_M | 17.1 GB | 25.00 | 26.93 | 0.083 | native packed GEMV (native_kquant=1; repeat 24.67) |
| Unsloth Q5_K_M | 19.8 GB | — | — | — | not re-run this pass |
| Unsloth Q6_K | 22.9 GB | 22.98 | 23.65 | 0.062 | native packed GEMV (native_kquant=1; repeat 22.93) |
| Unsloth Q8_0 | 29.0 GB | 34.77 | 35.58 | 0.038 | native Q8 SoA GEMV (native_q8=1; repeat 34.70) |
rapidllm bench -m /path/to/Qwen3.8-27B-Q4_K_M.gguf \
--device cuda --ctx 256 --max-new 16 --spec off --fuse=on --no-thinking --prompt 1,2,3Q8_0 community files may quantize DeltaNet in_proj_a / in_proj_b; the loader dequants those leftovers to F32. A_log / conv / norms still reject if quantized.
Re-measured 2026-08-16 on this box. Wall tok/s is after load + warmup generate. Thinking off. Prompt 1,2,3, 16 new tokens. --fuse=on --device cuda --spec off unless noted.
Use packed Q8_0. Official FP8 is next; on that checkpoint turn MTP on (--spec mtp --spec-n 3). Same-file official pair: MTP wall 35.94 (accepted=7/8) vs --spec off 27.19. Paris --max-new 64: MTP 41.68 (accepted=30/33) vs off 29.51. Do not use n-gram on the repeating 1,2,3 prompt — that inflates throughput.
# highest single-user tok/s
rapidllm bench -m /path/to/Qwen3.8-27B-Q8_0.gguf \
--device cuda --fuse=on --spec off --no-thinking --ctx 256 --max-new 16 --prompt 1,2,3
# official FP8 (if you must stay on the HF checkpoint)
rapidllm bench -m /path/to/Qwen3.8-27B-FP8 \
--device cuda --fuse=on --spec mtp --spec-n 3 --no-thinking --ctx 256 --max-new 16 --prompt 1,2,3| Weights | Wall / decode tok/s | Prefill s | Peak MiB | Notes |
|---|---|---|---|---|
| Unsloth Q8_0 | 35.56 | 0.038 | 33557 | packed native Q8 GEMV |
| Jackrong MTP-GGUF Q8_0 | 35.33 | 0.037 | 33169 | same official pair; beats FP8 30.73 |
| Official FP8 | 30.73 | 0.064 | 28007 | CUDA graph decode; skip unused F16 W workspace |
| Unsloth Q4_K_M | 26.51 | 0.085 | 24211 | smallest working set |
| Jackrong MTP-GGUF Q4_K_M | 26.23 | 0.080 | 22607 | native packed Q4/Q6; MTP accepted=3 but wall 21.93 |
| Jackrong MTP-GGUF Q6_K | 23.34 | 0.059 | 27615 | beats published Unsloth Q6 22.98 |
Official FP8 --spec mtp --spec-n 3 |
35.94 / 44.26 | 0.083 | 29341 | proposed=8 accepted=7; parallel GQA-6 flash + miss-fast; beats same-file off 27.19 |
--max-new 64 on official FP8 with --spec mtp is 41.68 wall (Paris prompt, accepted=30/33) vs same-file off 29.51. Leave n-gram off on benches: a repeating 1,2,3 prompt would let n-gram fake throughput.
Use official FP8, short ctx, continuous batch, independent prompts (--mixed). Packed Q8 is the single-user winner but not the multi-user one: residuals (wo / wd) stay serial packed GEMV (cublas add=1 zeros 27B tokens), so mixed Q8 tops out at 176 tok/s at --batch 64 (80 OOMs). FP8 cublasLt still wins by a wide margin. Official FP8 no longer keeps a 1.2 GiB F16 unpack workspace, so --batch 100 --mixed allocates. Equal-length mixed prefill now packs time-major T×B rows (one weight pass, prefill_eq_batch=1) on top of batched attn decode.
Greedy argmax used to launch <<<1,32>>>, so --batch >32 wrote token 0 for every slot ≥32. Remesured 2026-08-16 after a grid-stride fix: batch_zero_seqs=0 on 96 and 100.
rapidllm bench -m /path/to/Qwen3.8-27B-FP8 \
--device cuda --fuse=on --spec off --no-thinking --ctx 256 --max-new 16 \
--batch 100 --mixed --prompt 1,2,3--batch |
Mode | Wall tok/s (all seqs) | Per-seq | Peak MiB |
|---|---|---|---|---|
| 1 | — | 30.73 | 30.73 | 29159 |
| 8 | mixed | 193.6 | 24.2 | 30495 |
| 16 | mixed | 395.0 | 24.7 | 32031 |
| 32 | same prompt | 679.4 | 21.2 | 35103 |
| 32 | mixed | 683.7 | 21.4 | 35103 |
| 96 | mixed | 986.0 | 10.3 | 46279 |
| 100 | mixed | 1224.3 | 12.2 | 47233 |
--mixed = different prompts, one shared-weight decode step (the realistic multi-user path). Omit it for N copies of the same prompt. serve is still single-connection; this --batch / generate_batch path is the concurrent engine.
--batch may go up to 128; on this 48 GB card FP8 @ctx 256 fits 100. Rows 48/64/80 from before the argmax fix are omitted (slots 32+ were zero).
Q8 mixed, same flags, --batch N --mixed (2026-08-16, T≥16 Q8→F16 cublas on add=0; residuals packed GEMV):
--batch |
Wall tok/s | Decode tok/s | Per-seq | Peak MiB |
|---|---|---|---|---|
| 1 | 34.84 | 35.59 | 34.84 | 33561 |
| 8 | 77.55 | 92.83 | 9.69 | 34905 |
| 16 | 88.12 | 105.8 | 5.51 | 36457 |
| 32 | 131.9 | 158.3 | 4.12 | 39561 |
| 64 | 176.3 | 212.7 | 2.76 | 45793 |
| 80 | OOM | — | — | ~47700 |
Same-box official pair: prompt 1,2,3, 16 new tokens, thinking off, prefix-cache off. RapidLLM is --spec off --fuse=on --device cuda --ctx 256. Wall tok/s is after load + warmup generate.
vLLM is 0.21.1rc1.dev260+g10d264a2b, graphs on (enforce_eager=False, FULL + PIECEWISE). vLLM cannot load hybrid GGUF (qwen35 is not supported). GGUF rows therefore compare RapidLLM GGUF to the same family's official FP8 on vLLM. Qwen3.6 Q4_K / Q5_K were not on the box.
Q4_K / Q5_K / Q6_K stay packed and use native CUDA GEMV. Q8_0 uses the native Q8 GEMV. RAPIDLLM_REQUANT_KQUANT=1 restores the old requant-to-FP8 load path.
| Model | Weights | RapidLLM wall | RapidLLM decode | vLLM wall | vs vLLM |
|---|---|---|---|---|---|
| Qwen3.6-27B | Official FP8 | 31.67 | 32.04 | 19.36 | 1.635× |
| Qwen3.6-27B | Q6_K GGUF | 29.69 | 30.24 | 19.36 (FP8) | 1.533× |
| Qwen3.6-27B | Q8_0 GGUF | 26.23 | 27.40 | 19.36 (FP8) | 1.355× |
| Qwen3.8-27B | Official FP8 | 28.99 | 30.72 | 19.34 | 1.499× |
| Qwen3.8-27B | Q4_K_M GGUF | 25.00 | 26.93 | 19.34 (FP8) | 1.293× |
| Qwen3.8-27B | Q5_K_M GGUF | — | — | 19.34 (FP8) | not re-run |
| Qwen3.8-27B | Q6_K GGUF | 22.98 | 23.65 | 19.34 (FP8) | 1.188× |
| Qwen3.8-27B | Q8_0 GGUF | 34.77 | 35.58 | 19.34 (FP8) | 1.798× |
rapidllm bench -m /path/to/Qwen3.8-27B-FP8 \
--device cuda --ctx 256 --max-new 16 --spec off --fuse=on --no-thinking --prompt 1,2,3Did not run: Qwen3.6 Q4_K / Q5_K (weights not present). vLLM GGUF load of Qwen3.8-27B-Q4_K_M failed with GGUF model with architecture qwen35 is not supported yet.
Same official pair as above, remesured 2026-08-16 after the packed mixed-prefill change. SGLang is 0.5.17 in /home/znsoft/sglang-env (Engine, flashinfer, CUDA graphs on, language_only, speculative decode off). Wall tok/s is after load + warmup generate.
| Engine | Wall tok/s | n_new | Notes |
|---|---|---|---|
RapidLLM --spec off --fuse=on |
29.04 | 16 | tokens 170164 158534 … (non-zero) |
| SGLang 0.5.17 | 21.71 | 16 | output_ids [4, 5, 0, 31, 46474, …] repeating |
RapidLLM wall / SGLang wall = 1.34×. Do not treat RapidLLM decode_tok/s as this figure. SGLang did load the hybrid checkpoint (mamba cache active) and emit 16 tokens; the repeating low ids look like a hybrid / tokenizer mismatch, not a RapidLLM number. --spec mtp on the official pair is a wall win: 35.94 tok/s (accepted=7/8) vs same-file --spec off 27.19.
Same box (RTX 6000 Ada 48 GB), same official pair (short prompt, 16 new, graphs on). Numbers above.
Where RapidLLM is ahead
- Single-request decode. Official FP8 wall tok/s is 1.50–1.64× vLLM (3.8: 28.99 vs 19.34; 3.6: 31.67 vs 19.36). Native Q8_0 is 1.80× the same vLLM FP8 run (34.77). Decode is fused C++/CUDA with no Python in the hot path.
- Hybrid GGUF. vLLM 0.21.1rc1 cannot load
qwen35GGUF. RapidLLM runs Q4_K / Q5_K / Q6_K / Q8_0 on the same IR as official FP8, so a 32 GB / 48 GB box can stay on packed weights. - This hybrid architecture. The engine owns
layer_types[]: 48 O(1) DeltaNet states + 16-layer KV. vLLM treats the model as a generic serving graph; RapidLLM's dual cache and fused GDN/RMS/MLP are written for this 3:1 mix. - Long-window allocation. Without TurboQuant, RapidLLM allocates 163840. With
--kv-type q8k_tq3vit allocates 262144 on 48 GB. vLLM on this card failed a 200k load (KV 12.39 GiB > 12.05 GiB free atgpu_memory_utilization=0.90). - Short-fill decode at a large window. 128k allocated, 256-token fill: RapidLLM decode 27.43 tok/s vs vLLM wall 17.54.
- Local / embeddable. C API + CLI + one-process
serve. No PyTorch runtime. CPU AVX2/AVX-512 path exists; vLLM does not.
Where vLLM is ahead (do not oversell)
- Long prefill. At 128k / 2048–8192 fill, vLLM wall tok/s is higher (7.90 vs 7.17; 2.44 vs 1.93). RapidLLM prefill attention on the 16 gated layers is still O(N²).
- Multi-user serving. vLLM pages independent sequences, continuous-batches different prompts, and speaks a production OpenAI HTTP stack. RapidLLM
--batch Nis same-prompt weight sharing;serveis single-connection (SSE streaming is supported). - Ecosystem. Sampling, tools, LoRA, and multi-GPU TP/PP are vLLM's job. RapidLLM is a single-GPU / CPU engine for this one architecture family.
Pick RapidLLM for local Qwen3.6 / 3.8-27B decode (especially GGUF, 128k–262k allocation, or no Python). Pick vLLM for a shared endpoint with mixed prompts and long prefills.
Official FP8, --device cuda --fuse=on --spec off --no-thinking. Wall tok/s after warmup generate. RapidLLM CUDA KV is FP32 (~128 KiB / token). GPU is 49140 MiB.
Allocate --ctx, then generate 8 new tokens from prompt 1,2,3 (3-token fill). This measures the largest window that fits, not a full-window prefill.
--ctx |
Result | Peak MiB | Decode tok/s | Wall tok/s |
|---|---|---|---|---|
| 131072 | ok | 44292 | 32.07 | 29.11 |
| 147456 | ok | 46340 | 32.03 | 28.85 |
| 163840 | max without TurboQuant | 48388 | 31.97 | 28.56 |
| 167936 | CUDA OOM at session | — | — | — |
| 172032 | CUDA OOM at session | — | — | — |
| 200000 | not tried (above OOM) | — | — | — |
--ctx 163840 + --prompt-n 256 + 8 new: prefill 2.69 s, decode 27.83 tok/s, wall 2.72 tok/s (prefill dominates). Peak still 48388 MiB.
--ctx 256 is ~27.8 GiB; each extra 16384 tokens of KV is ~2.00 GiB. 167936 needs ~0.5 GiB more than 163840 and does not fit.
serve is single-connection; this engine's concurrent path is --batch N (optional --mixed for independent prompts, one shared weight pass). --batch may go to 128; on 48 GB the short-ctx FP8 cap is 100 (see peak launch recipes).
See the peak launch recipes table (2026-08-16). Short-ctx official FP8 remesures at 1224.3 wall tok/s at --batch 100 --mixed with every sequence non-zero (prefill_eq_batch=1 T=3 B=100). The older ~94 tok/s plateau was before batched GDN / cublasLt T=8 / GPU argmax.
--ctx 262144 with TurboQuant (RAPIDLLM_KV_TQ=1 / --kv-type q8k_tq3v, auto when --ctx>163840) allocates on 48 GB: K q8 + V tq3 persist ~5.7–6.1 GiB/seq plus an 8k F16 scratch window. Full F16 at 262k is ~16–17 GiB/seq and does not fit. Re-measured 2026-08-15 on the same RTX 6000 Ada 48 GB box, concurrency swept until aggregate TPS stopped rising (batch 3 OOMs on every format). Flags: --device cuda --fuse=on --spec off --no-thinking --max-new 16 --prompt 1,2,3 --kv-type q8k_tq3v, continuous batch (--batch N, one shared weight pass). CUDA graphs are on (Q8 capture fails and falls back to eager). Q4/Q6 use the default requant-to-FP8 GEMV; Q8 stays packed. --spec is off because a repeating 1,2,3 prompt would let n-gram fake throughput. tok/s is aggregate. Peak MiB is cuda_mem_ready after graphs.
| Weights | --batch |
Wall tok/s | Decode tok/s | Per-seq decode | Peak MiB | Result |
|---|---|---|---|---|---|---|
| Official FP8 | 1 | 29.06 | 30.81 | 30.81 | 35925 | ok |
| Official FP8 | 2 | 40.76 | 43.91 | 21.96 | 42325 | max that fits |
| Official FP8 | 3 | — | — | — | 46857 | CUDA OOM at session |
| Q4_K_M | 1 | 31.15 | 31.56 | 31.56 | 38991 | ok |
| Q4_K_M | 2 | 49.03 | 52.15 | 26.07 | 45781 | ok |
| Q4_K_M | 3 | — | — | — | — | CUDA OOM at session |
| Q6_K | 1 | 30.44 | 31.04 | 31.04 | 39761 | ok |
| Q6_K | 2 | 46.68 | 49.80 | 24.90 | 46551 | ok |
| Q6_K | 3 | — | — | — | — | CUDA OOM at session |
| Q8_0 | 1 | 34.88 | 35.71 | 35.71 | 40631 | ok (eager; graph capture failed) |
| Q8_0 | 2 | 55.50 | 59.54 | 29.77 | 47421 | highest aggregate TPS |
| Q8_0 | 3 | — | — | — | — | CUDA OOM at session |
Per-format peak at 262k (short prompt, continuous batch):
| Weights | Max --batch |
Peak wall tok/s | Peak decode tok/s |
|---|---|---|---|
| Official FP8 | 2 | 40.76 | 43.91 |
| Q4_K_M | 2 | 49.03 | 52.15 |
| Q6_K | 2 | 46.68 | 49.80 |
| Q8_0 | 2 | 55.50 | 59.54 |
Fill-256 (still inside the 8k F16 window) at --ctx 262144, same box and flags, earlier run. Q6/Q8 prefill stays on packed GEMM, so wall tok/s drops even when decode is fast:
| Weights | --batch |
Wall tok/s | Decode tok/s | Prefill s | Peak MiB |
|---|---|---|---|---|---|
| Official FP8 | 1 | 26.99 | 30.36 | 0.066 | 35925 |
| Official FP8 | 2 | 37.95 | 44.87 | 0.065 | 42325 |
| Q4_K_M | 1 | 23.97 | 31.17 | 0.109 | 38991 |
| Q4_K_M | 2 | 39.08 | 53.11 | 0.108 | 45781 |
| Q6_K | 1 | 9.16 | 30.74 | 0.646 | 39761 |
| Q6_K | 2 | 16.65 | 51.15 | 0.648 | 46551 |
| Q8_0 | 1 | 2.65 | 35.31 | 2.817 | 40631 |
| Q8_0 | 2 | 5.13 | 60.46 | 2.856 | 47421 |
On this 48 GB card the 262k concurrency cap is --batch 2 for FP8 / Q4 / Q6 / Q8. Peak total TPS is 55.50 tok/s (Q8_0, batch 2, 3-token prompt). Official FP8 peaks at 40.76 tok/s (batch 2). Batch 3+ OOMs because persist KV scales linearly (~5.7–6.1 GiB/seq).
--ctx 245760 (240×1024) on official FP8, remesured 2026-08-17, same box and flags as the 262k sweep (--kv-type q8k_tq3v, --spec off, prompt 1,2,3, 16 new). F16 KV OOMs at session. TQ persist is 5370 MiB/seq. seq_cap=245760.
--batch |
Wall tok/s (总) | Decode tok/s | Per-req wall | Per-seq decode | Peak MiB | Result |
|---|---|---|---|---|---|---|
| 1 | 27.20 | 29.72 | 27.20 | 29.72 | 36131 | ok |
| 2 | 47.90 | 54.84 | 23.95 | 27.42 | 42685 | max that fits; zero_seqs=0 |
| 3 | — | — | — | — | — | CUDA OOM at session |
Concurrency cap is --batch 2. Aggregate TPS is highest at B=2 (1.76× B=1 wall). Per-request TPS drops 27.20 → 23.95 because two sequences share one weight pass but still pay two persist KV streams.
# max window that allocates on 48 GB without TurboQuant
rapidllm bench -m /path/to/Qwen3.8-27B-FP8 \
--device cuda --ctx 163840 --max-new 8 --spec off --fuse=on --no-thinking --prompt 1,2,3
# 262k + TurboQuant + continuous batch (highest official-FP8 total TPS)
RAPIDLLM_KV_TQ=1 rapidllm bench -m /path/to/Qwen3.8-27B-FP8 \
--device cuda --ctx 262144 --batch 2 --max-new 16 --spec off --fuse=on --no-thinking --prompt 1,2,3
# 262k peak aggregate TPS on this box
RAPIDLLM_KV_TQ=1 rapidllm bench -m /path/to/Qwen3.8-27B-Q8_0.gguf \
--device cuda --ctx 262144 --batch 2 --max-new 16 --spec off --fuse=on --no-thinking --prompt 1,2,3
# short-ctx concurrent aggregate TPS (peak on this 48 GB box)
rapidllm bench -m /path/to/Qwen3.8-27B-FP8 \
--device cuda --ctx 256 --batch 100 --mixed --max-new 16 --spec off --fuse=on --no-thinking --prompt 1,2,3--image PATH loads PNG / JPEG / PPM / BMP, runs the in-tree ViT (CPU), and splices vision_start + image_pad × N + vision_end in front of the prompt. Generate replaces those image_pad embeddings with the encoder output (CPU and CUDA). Needs an HF checkpoint that still has visual.* (pass --image so they are not skipped).
rapidllm -m /path/to/Qwen3.8-27B-FP8 --image photo.png \
--prompt "What is in this image?" --max-new 64 --device cuda --spec offVideo is not supported.
Same box as the short-prompt bakeoff: NVIDIA RTX 6000 Ada Generation, 49140 MiB. Official FP8 text weights ~28 GiB. RapidLLM CUDA KV is FP32 (about 128 KiB / token across the 16 gated-attention layers). vLLM uses paged FP16 KV (about 64 KiB / token).
All RapidLLM rows below are --device cuda --fuse=on --spec off --no-thinking (decode CUDA graph + fused GDN/RMS/MLP). --spec auto is not used here: a repeating prompt would let n-gram fake throughput.
--prompt-n N fills N non-repeating ids. tok/s is wall (prefill + 8–16 new tokens). decode_tok/s is the decode-only rate after that fill. Prefill tok/s = N / prefill_s.
Window --ctx |
Filled tokens | Prefill s | Prefill tok/s | Decode tok/s | Wall tok/s (8–16 new) | Notes |
|---|---|---|---|---|---|---|
| 256 (short bakeoff) | 3 | 0.038 | — | 32.03 | 31.61 | keep; vs vLLM 19.36 = 1.63× |
| 4096 | 64 | 0.60 | 107 | 31.84 | 9.74 | still the short-ctx attn kernel |
| 16384 | 2048 | 23.68 | 86.5 | 15.64 | 0.33 | old T=4 GEMM (superseded below) |
| 131072 | 256 | 0.073 | 3500 | 27.43 | 24.35 | cublasLt FP8 + F32 GQA-GEMM attn; vs vLLM 17.54 |
| 131072 | 2048 | 0.70 | 2920 | 16.89 | 7.17 | two 1024-token tiles; vs vLLM 7.90 (still short) |
| 131072 | 4096 | 1.49 | 2746 | 11.83 | 3.84 | 4×1024 tiles; no paired vLLM |
| 131072 | 8192 | 3.21 | 2550 | 7.44 | 1.93 | vs vLLM 2.44 |
| 131072 | 16384 | 7.22 | 2270 | 4.27 | 0.90 | 16×1024 tiles; tokens non-zero |
| 131072 | 131056 | — | — | — | — | full fill not useful (O(N²) on 16 attn layers) |
| 200000 | 256 | — | — | — | — | CUDA OOM. Measured max alloc for 3.8-FP8 is 163840 |
vLLM on this card reported that 200k needs 12.39 GiB KV vs 12.05 GiB free at gpu_memory_utilization=0.90 (estimated max len 194432).
Same token-id pattern, 8 new tokens, wall tok/s includes prefill:
| Window | Filled tokens | Wall s | Wall tok/s | vLLM logged in/out tok/s |
|---|---|---|---|---|
| 131072 | 256 | 0.456 | 17.54 | in 563 / out 17.6 |
| 131072 | 2048 | 1.013 | 7.90 | in 1738 / out 6.8 |
| 131072 | 8192 | 3.274 | 2.44 | in 2504 / out 2.45 |
| 200000 | — | — | — | load failed: KV 12.39 GiB > 12.05 GiB |
Short official pair (prompt 1,2,3, 16 new): vLLM 19.36 tok/s (3.6-27B FP8, graphs on). See the vs-vLLM table above.
Qwen3.6 Q6_K / Q8_0 and Qwen3.8 Q4_K / Q5_K / Q6_K / Q8_0 all load and decode at --ctx 256 on this 48 GB card (see the vs-vLLM table above). Embeddings are dequantized to FP32 (~5 GiB). Q4–Q6 requant to the FP8 GEMV path; Q8 stays packed.
Long-ctx GGUF on 48 GB is still tight: Q8 packed weights (~28–29 GiB) + FP32 embed + FP32 KV leave little room past a short window. Use official FP8 (or Q4/Q5) for 128k allocation benches.
# RapidLLM 128k window, 8k fill (fits 48 GB)
rapidllm bench -m /path/to/Qwen3.6-27B-FP8 \
--device cuda --ctx 131072 --prompt-n 8192 --max-new 8 \
--spec off --fuse=on --no-thinking
# vLLM 128k window (graphs on)
# see scratch vllm_long.py: max_model_len=131072, TokensPrompt, 8 new tokensFull 128k/200k fills are not a useful default: RapidLLM prefill attn is still O(N²) on the 16 gated-attention layers (8k fill already 121 s). Decode at a 128k allocation with a short fill is the number that isolates the window cost.
Qwen3.6 / 3.8-27B ship an embedded MTP head (mtp.fc, mtp.norm, mtp_num_hidden_layers=1). RapidLLM parses it and runs it as the speculative draft. There is no external draft-model path: MTP replaces that.
--spec auto (the default) picks a draft source in this order:
- The target's own MTP head, if present
- N-gram continuation from already generated tokens
rapidllm -m /path/to/Qwen3.8-27B-FP8 --spec mtp --spec-n 3 --prompt "Hello"T=2 verify reads each weight once (custom GEMV, not cublasLt n=2). A fused miss keeps token-0 hidden/KV and restores snapped GDN S/conv instead of re-decoding. On official Qwen3.8-27B-FP8 this beats --spec off: 35.94 vs 27.19 tok/s on prompt 1,2,3 (accepted=7/8). Decode uses in-kernel Flash Attention for GQA 24/4 (group=6); RAPIDLLM_NO_FLASH=1 is the kill switch.
--batch N runs N copies of the same prompt with one shared weight pass per decode step. On CUDA, set RAPIDLLM_MAX_BATCH (the CLI does this from --batch). CPU falls back to N sequential generates.
rapidllm bench -m /path/to/Qwen3.8-27B-FP8 --device cuda --batch 4 --spec offC API: rapidllm_generate_batch. Session API: Session::generate_batch.
rapidllm serve is a single-connection HTTP server. Set "stream": true for SSE token streaming. Thinking is off.
| Method | Path | Protocol |
|---|---|---|
GET |
/health, /v1/health |
liveness |
GET |
/v1/models |
model list |
POST |
/v1/chat/completions |
OpenAI Chat Completions |
POST |
/v1/responses |
OpenAI Responses |
POST |
/v1/messages |
Anthropic Messages |
curl http://127.0.0.1:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"qwen","messages":[{"role":"user","content":"Hello"}],"max_tokens":64}'
curl -N http://127.0.0.1:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"qwen","messages":[{"role":"user","content":"Hello"}],"max_tokens":64,"stream":true}'max_tokens is clamped to 4096. temperature <= 0 is greedy. "stream": true emits text/event-stream (OpenAI chat chunks + [DONE], Responses response.output_text.delta, Anthropic content_block_delta).
#include "rapidllm/api.h"
RapidConfig cfg = {0};
cfg.model_path = "model.gguf";
cfg.device = "cpu";
cfg.ctx = 32768;
cfg.fuse = 1;
cfg.language_only = 1;
RapidError err = {0};
RapidLLM* eng = rapidllm_load(&cfg, &err);
RapidSessionConfig sc = {0};
sc.enable_thinking = 1;
sc.max_new_tokens = 64;
sc.spec = 3; /* auto → MTP if the model has it */
RapidSession* sess = rapidllm_session_new(eng, &sc, &err);
int32_t ids[256], out[64];
int n = rapidllm_encode(eng, "Hello", ids, 256, &err);
RapidSampleParams sp = {0};
sp.greedy = 1;
int got = rapidllm_generate(sess, ids, n, &sp, out, 64, &err);
/* Same prompt, N sequences, one shared weight pass per step (CUDA). */
int out_n[4];
int32_t bout[4 * 64];
rapidllm_generate_batch(sess, ids, n, 4, &sp, bout, 64, out_n, &err);
rapidllm_session_free(sess);
rapidllm_free(eng);RAPIDLLM_API_VERSION is 1. See include/rapidllm/api.h.
CMake generates a synthetic hybrid fixture (python/goldens/tiny_hybrid.py) and wires CTest:
ctest --test-dir build --output-on-failure| Test | What it checks |
|---|---|
test_ir |
ModelDesc, layer_types[], VisionDesc |
test_loader |
HF FP8 + GGUF + reject-bad fixtures + visual name map |
test_hybrid |
greedy tokens vs golden, fuse on/off, both formats |
test_simd_bench |
AVX2 / AVX-512 vs scalar |
test_nv |
DP4A GEMV reference |
test_spec |
n-gram / MTP speculative decode |
test_batch |
continuous batch generate |
test_protocol |
OpenAI / Anthropic parse + render + /health |
test_cuda_decode |
CUDA path or host-ref fallback |
include/rapidllm/ public headers (C API, IR, runtime, kernels, server)
src/api/ extern "C" implementation
src/cli/ rapidllm, rapidllm_version
src/server/ HTTP serve (OpenAI + Anthropic)
src/frontend/ HF safetensors, GGUF, name map, WeightStore
src/ir/ ModelDesc + VisionDesc
src/kernels/ scalar / AVX2 / AVX-512 / fused / vision / CUDA
src/runtime/ session, DualCache, planner, tokenizer, sampler
src/backend/cpu/ CpuDevice
shaders/ GLSL compute (not wired into the default build)
python/goldens/ tiny hybrid fixture + golden tokens
tests/ unit / golden / loader / spec / batch / protocol
docs/ architecture (EN/ZH)
serveis single-connection (no concurrent requests). SSE (stream: true) is supported.- No video encoder
- No MoE expert path
- No ARM / Apple Silicon
- No linking or vendoring llama.cpp / ggml / FLA
Engine: MIT
Model weights: Apache-2.0 (Qwen)