Qwen3.8-27B-FP8¶
Runtime status¶
Validated native C++/CUDA TP4 text runtime. PocketLLM detects the nested Qwen3.5 text configuration, maps rank-local Safetensors weights, converts BF16 scales/non-FP8 tensors to FP16 for Turing where required, and keeps local FP8 weights resident on each GPU.
The current integration supports text prompt/token-ID smoke, timed greedy generation, and native OpenAI-compatible text serving through pocketllm_engine. It does not execute the checkpoint's vision tower or accept image/video inputs.
Model specification¶
The validated checkpoint reports:
| Field | Value |
|---|---|
| HF architecture | Qwen3_5ForConditionalGeneration |
| Text model type | qwen3_5_text |
| Text layers | 64 |
| Gated DeltaNet layers | 48 |
| Full GQA layers | 16 |
| Hidden size | 5120 |
| Dense MLP intermediate | 17,408 |
| Vocabulary | 248,320 |
| Maximum positions | 262,144 |
| Query heads | 24 |
| KV heads | 4 |
| Head dimension | 256 |
| Partial RoPE | 64 dimensions (factor 0.25) |
| Linear-attention key heads | 16 × 128 |
| Linear-attention value heads | 48 × 128 |
| Convolution kernel | 4 |
| Quantization | FP8 E4M3, dynamic activation scheme |
| Weight scale block | 128×128 |
The root config also contains a vision tower, but PocketLLM deliberately dispatches only the text model tensors.
Implemented execution path¶
- Nested Qwen config detection and strict tensor/scale shape validation.
- TP4 rank-local embedding, head, attention, and dense MLP sharding.
- FP8 E4M3 weights stored as bytes with FP16 block scales on RTX 2080 Ti.
- Online FP8 unpacking in CUDA tiles/registers; no full FP16/FP32 weight expansion.
- Separate multi-row prefill and single-token decode projection kernels.
- 48-layer Gated DeltaNet sequence/recurrent kernels with persistent state and convolution tails.
- 16-layer GQA prefill and KV-cache decode with local K/V heads.
- FP16 activation storage with FP32 local accumulation/state where required; no prompt-length FP32 activation expansion.
- Chunked prefill (default 8,192 tokens,
--prefill-chunk-tokens) that retains only recurrent state, convolution tails, and full-attention KV cache between chunks. - Exact single-request prefix reuse: the position-indexed GQA KV cache and the DeltaNet recurrent state are retained across sequential
prefill()calls. Appended prompts execute only their uncached suffix; diverging or compressed prompts restore a device-resident recurrent snapshot at the longest safe common prefix. - FP16 KV cache by default, plus explicit opt-in FP8 E4M3 cache with per-token/KV-head FP16 scales over 64-channel blocks.
- Decode-only fused FP8 gate/up projection plus SwiGLU.
- Exact FP16 GQA kernels, on by default: tiled prefill and split-context fused decode with compact online-softmax partials. On SM75 the split/merge decode path is dispatched from context 4,096 onward, and the reference score/value decode below that, where it is faster.
POCKETLLM_QWEN_GQA_OPTIMIZED=0restores the reference path. - Opt-in FP16 sink-plus-sliding-window attention through
--qwen-attention-window Nand optional--qwen-attention-sink-tokens N. This changes full-attention semantics and is not part of exact parity or default performance claims; FP8 cache is intentionally rejected for this mode. - TP4 NCCL reductions and global greedy top-1 selection.
- Opt-in native one-layer MTP loading and greedy speculative generation through
--qwen-mtp-tokens K. The MTP layer reuses the target embedding/LM head, recursively proposes drafts, and verifies[current_token, draft_1, ..., draft_K]in one multi-row target forward. Partial rejection restores DeltaNet state/convolution tails and replays only the committed input prefix. MTP remains disabled by default. - Opt-in external Qwen DSpark loading through
--qwen-dspark PATH. The real five-layer BF16 drafter is replicated on every TP rank, consumes target post-layer taps4,16,28,40,52, proposes the checkpoint's fixed seven-token block, and verifies[anchor,draft_1,...,draft_7]in one eight-row target forward. Its position-indexed context K/V follows the target prefix cache across append, shorter-prefix, branch, and compressed-context restores.
Validated performance¶
Hardware: 4×RTX 2080 Ti 22 GiB, TP4 (GPU 0–3), single request, real Qwen3.8-27B-FP8 checkpoint and
deterministic real-tokenizer prompts. One serial sweep of the engine defaults — complete 64 layers,
prefill_chunk_tokens=8192, FP16 KV cache, greedy sampling, 128 generated tokens — on master
cfad866 (2026-09-15). Run record: .tmp/qwen_fp8_page_20260915/.
| Prompt | Prefill | Decode | KV data/rank | Peak activation workspace | Rank parity |
|---|---|---|---|---|---|
| 64 | 115.91 tok/s (0.55 s) | 45.05 tok/s | 3.0 MiB | 7.9 MiB | PASS |
| 512 | 864.54 tok/s (0.59 s) | 43.22 tok/s | 10.0 MiB | 63.0 MiB | PASS |
| 4,096 | 1,729.05 tok/s (2.37 s) | 43.82 tok/s | 66.0 MiB | 504.2 MiB | PASS |
| 8,192 | 1,818.65 tok/s (4.50 s) | 43.99 tok/s | 130.0 MiB | 1,008.4 MiB | PASS |
| 32,768 | 1,673.79 tok/s (19.58 s) | 41.77 tok/s | 514.0 MiB | 1,008.4 MiB | PASS |
| 65,536 | 1,453.51 tok/s (45.09 s) | 39.11 tok/s | 1,026.0 MiB | 1,008.4 MiB | PASS |
Decode holds near 44 tok/s out to 8,192 tokens and reaches 39.11 tok/s at 65,536. Prefill peaks at 8,192 tokens and is still at 80% of that peak at 65,536.
Read the table with the two reporting rules from ../guides/benchmarking.md:
- Prefill and decode are reported separately; neither is a wall-clock figure, and the prefill seconds above exclude model load and process startup.
- Decode throughput is only comparable between runs that generate the same number of tokens. These runs generate 128, so the timed decode window excludes the first token (produced by prefill) and is long enough to average out per-step overhead. Any measurement generating four or 24 tokens is not comparable to this table's decode column, including the earlier figures on this page.
Prefill is close to linear in prompt length above 4,096 tokens: the 4,096→32,768 segment runs at a marginal 1,670 tok/s (a 600 µs/token slope) and the 32,768→65,536 segment at 1,285 tok/s, the difference being the growing quadratic attention term. The 64- and 512-token rows measure short-prompt latency, not steady-state throughput: both complete in 0.55–0.59 s because a fixed per-process cost dominates at that size.
Per rank, 6.86 GiB of resident weights and 0.71 MiB of scales are fixed; only the KV data and the activation workspace grow with context, which is why the memory curve is nearly flat. The activation workspace is the peak capacity of the reusable chunk workspace and does not grow with prompt length past one chunk.
Earlier revisions of this section published 416.48, 453.08 and 295.46 tok/s for a 512-token fixture in three different places. All three predate the GQA tensor-core prefill and cuBLAS FP8 prefill defaults, and the long-context tables used 512-token prefill chunks; this sweep supersedes them.
Current memory-safe FP16-activation kernels¶
The current reference runtime now uses FP16-input, FP32-accumulation FP8 projection kernels without expanding prompt activations or weights. The prefill path uses a 128-token x 64-output N64 tile when alignment and batch size permit; decode uses vectorized single-row FP8 matvec, while the original scalar kernel remains the fallback. Two-row and four-row decode variants remain explicit experiments because their register pressure reduced end-to-end decode throughput. These kernels preserve the default exact full-attention semantics and FP16 KV cache.
These kernels are the default projection path. Their end-to-end result is the sweep in Validated
performance above, and the resident weight and scale bytes they report are 7,367,270,656 and
742,400 per rank.
The direct FP16-activation FP8 projection gate covers aligned and padded strides, masked rows, tail K tiles, vectorized-versus-scalar decode dispatch, and the wide prefill tile. It reports decode max absolute error 1.459e-2 against the FP32 host reference, with vectorized-versus-scalar output difference 0; the 4-row experimental path differs by at most 3.906e-3. The focused FP8 online operator suite and full TP4 rank parity checks also pass.
Native MTP speculative decoding¶
The checkpoint declares mtp_num_hidden_layers=1 and ships the predictor in mtp.safetensors. PocketLLM maps this full-attention layer under TP4, uses the required [normalized_embedding, normalized_hidden] fusion order, consumes the target's final-normalized hidden state, and uses the MTP layer's normalized output as the recursive hidden. Target verification computes all candidate-row logits together and performs one batched TP global top-1 collective.
Enable it explicitly with:
--qwen-mtp is equivalent to enabling the default K=1. MTP requires all 64 target layers; combining it with a nonzero partial --smoke-layers value is rejected. It is also context-safe: the prompt plus requested output count must fit in max_context; each speculative block is capped by the remaining output count, so its temporary verify suffix stays within that bound. The runtime reports mtp_accept_rate, proposed/correct draft counts, rollback/replay counts, and separate prefill/draft/verify/replay seconds.
This MTP implementation follows the standard Qwen3.5 shifted-hidden predictor path used by vLLM: prompt target rows are paired with their next-token inputs to prime an independent absolute-position MTP KV cache, after which the predictor advances recursively. SGLang's newer frozen_kv_mtp worker is a separate Gemma-oriented optimization that requires a model-specific mapping from assistant layers to target KV-owner layers; Qwen3.5 does not expose such a mapping, so target and MTP KV are not aliased.
The optimized target verifier reuses each FP8 weight row across all 2–8 candidate rows, fuses small-batch gate/up/SwiGLU, reuses transaction buffers, and batches exact GQA while retaining the reference score/softmax/value reduction order. An even faster split online-softmax verifier is available through QWEN_GQA_VERIFY_SPLIT=1, but stays experimental: its small numerical drift can change greedy output at near-tie logits even though direct attention error is below 8e-6.
Real TP4, full 64-layer, 64-token generation results below cover several tokenizer-real 512-token prompts. Every TP rank agreed and each MTP sequence matched its serial plain run. The final adaptive policy starts at K=1, doubles K after full acceptance, and backs off after rejection:
| Prompt | Acceptance | Plain TPS | Adaptive K<=4 TPS | Decode speedup | Wall speedup |
|---|---|---|---|---|---|
| Repeated natural language | 100.0% | 37.19 | 79.39 | 2.135x | 1.331x |
| Config/model text | 94.0% | 36.75 | 69.61 | 1.894x | 1.273x |
| Source code | 70.0% | 36.91 | 50.66 | 1.372x | 1.106x |
| README prose | 70.9% | 36.83 | 48.10 | 1.306x | 1.079x |
| Model documentation | 64.8% | 36.93 | 43.53 | 1.179x | 1.026x |
The optimized path therefore clears 1.5x only when draft acceptance is high (94% or better in the measured cases); it cannot honestly guarantee 1.5x for arbitrary prompts. Starting adaptive mode at K=1 protects mixed prompts better than starting at K=4, but 65–71% acceptance still yields only 1.18–1.37x decode speedup. A target top1-top2 logit-margin gate was also tested and rejected: thresholds 0.5 and 1.0 reduced performance on the difficult prompts, so no margin-gating code or CLI option is retained. The one-shot wall result includes independent-MTP prompt priming; persistent single-concurrency requests with exact prefix reuse remain the intended workload.
The parity-safe exact-GQA verifier now uses a warp-tiled value pass: one warp owns one candidate row, three query heads, and 32 value channels while preserving each output element's left-to-right FP32 accumulation order. Fresh 100%-acceptance measurements show the long-context gain increasing as plain decode becomes attention-bound:
| Prompt | Plain TPS | MTP K=4 TPS | Decode speedup | One-shot wall speedup | Parity |
|---|---|---|---|---|---|
| 4,096 | 30.10 | 50.17 | 1.667x | not reported | PASS |
| 8,192 | 24.78 | 61.29 | 2.474x | 0.961x | PASS |
| 32,768 | 11.30 | 36.26 | 3.208x | 0.929x | PASS |
The 8K/32K one-shot wall numbers remain below 1x because each separately launched MTP process primes an additional full-prompt predictor KV cache; this is not repeated for an exact-prefix hit in the long-lived workload. An opt-in split-GQA experiment measured 2.38–4.57x at 512/4K/8K/32K, but it is not included in the parity-safe claim because a separate near-tie prompt exposed sequence drift.
Reproduce serial plain/K=1/K=2/K=4 A/B cases with real tokenizer IDs and automatic TP-rank/plain parity checks:
python scripts/bench_qwen_mtp.py \
--ckpt /path/to/Qwen3.8-27B-FP8 \
--tp-world 4 --devices 0,1,2,3 \
--lengths 512,32768 \
--mtp-tokens 1,2,4 \
--max-new-tokens 32 \
--layers 0 \
--tokenizer-python /path/to/deepseek/bin/python
For the recommended adaptive policy, pass one maximum K and --adaptive:
python scripts/bench_qwen_mtp.py \
--ckpt /path/to/Qwen3.8-27B-FP8 \
--tp-world 4 --devices 0,1,2,3 \
--lengths 512,8192,32768 \
--mtp-tokens 4 --adaptive \
--max-new-tokens 64 --layers 0 \
--tokenizer-python /path/to/deepseek/bin/python
External DSpark speculative decoding¶
The external checkpoint RadixArk/Qwen3.8-27B-DSpark (epoch_2_step_4166) is supported as an explicit opt-in:
The directory must contain its config.json and single model.safetensors. The implementation validates all 62 expected BF16 tensors, materializes them as FP16 on SM75, and adds 2,623,214,594 resident weight bytes per rank. The five-layer draft backbone is replicated rather than tensor-parallel; only target embedding/head operations and the vocabulary-sharded Markov w2 use TP collectives. Native MTP and external DSpark are mutually exclusive, and DSpark requires all 64 target layers.
The checkpoint fixes block_size=7, so each transaction proposes seven drafts and target-verifies eight rows: [anchor,draft_1,...,draft_7]. The target post-layer taps are 4,16,28,40,52. Partial rejection restores DeltaNet recurrent state and convolution tails, crops logical target/DSpark K/V to the committed position, then replays only [anchor,accepted drafts]. A remaining output tail shorter than eight tokens uses ordinary exact decode rather than changing the checkpoint's block semantics.
The confidence head is evaluated for every draft and reported as dspark_confidence_count/mean/min/max, but it does not currently change the static width-8 schedule: neither the checkpoint nor its published static deployment supplies a validated confidence threshold. Speculative accounting retains the existing mtp_* field names for CLI compatibility (mtp_accept_rate, proposed/correct drafts, rollback/replay, and stage seconds).
Real TP4, full-model, FP16-target-KV A/B results below use the same tokenizer-real deterministic language fixture, 64 generated tokens, plain then DSpark serial execution, and exact DSpark-versus-plain plus all-rank token checks. The table's draft match rate is correct_drafts / proposed_drafts and excludes the target bonus token; it is not the model-card spec_accept_length, whose numerator includes one bonus token per verification step.
| Prompt | Draft match rate | Plain TPS | DSpark TPS | Decode speedup | Plain wall | DSpark wall | Wall speedup | Highest DSpark rank memory | Parity |
|---|---|---|---|---|---|---|---|---|---|
| 512 | 94.64% | 37.20 | 60.02 | 1.614x | 2.944 s | 2.385 s | 1.235x | 11.19 GB | PASS |
| 8,192 | 96.43% | 24.80 | 43.79 | 1.766x | 31.505 s | 31.551 s | 0.999x | 11.61 GB | PASS |
| 32,768 | 96.43% | 11.31 | 21.70 | 1.919x | 295.812 s | 295.773 s | 1.000x | 12.52 GB | PASS |
Long-context one-shot wall time is prefill-bound. DSpark's target-feature projector and five layers of context K/V projection make prefill slightly slower even though decode gets progressively faster. This is why the intended deployment remains a long-lived, single-request prefix-reusing engine rather than repeatedly paying cold prefill.
Acceptance is workload-sensitive. The earlier 17% figure came from a deliberately bare 16-token prompt, not the DSpark model-card benchmark protocol: it omitted the Qwen chat template, used greedy decoding, and counted only draft matches. Re-running the same wording with the real chat template still produced a difficult greedy case (15.34% draft match without thinking, 23.81% with thinking), so it is a valid stress case but not evidence that the published DSpark acceptance is 17%. The model card instead reports spec_accept_length including the bonus token, with a request-weighted mean of 3.39 accepted tokens per verification step over 1,164 sampled requests (macro-average 3.35) at temperature 0.6, top-k 20, top-p 0.95, thinking enabled, and 2,048 generated tokens. Our native path is currently greedy and should not be compared to those stochastic benchmark numbers as if they were the same metric. DSpark remains default-off pending benchmark-protocol-matched measurements.
For reference, the bare greedy stress prompt measured 31 correct drafts out of 182 proposals (31/182 = 17.03%), 17.42 tok/s DSpark decode, and exact plain-token parity. The chat-template, thinking-enabled rerun measured 35/147 (23.81%).
Reproduce the protocol-sensitive chat-template fixture with AutoTokenizer.apply_chat_template(..., add_generation_prompt=True, enable_thinking=True) before passing token IDs to the C++ runtime; do not use the raw user sentence as the benchmark prompt.
DSpark therefore remains default-off; enable it only after measuring representative traffic with the intended chat template, sampling policy, and generation length.
Reproduce the short/long serial A/B with:
/path/to/deepseek/bin/python scripts/bench_qwen_dspark.py \
--ckpt /path/to/Qwen3.8-27B-FP8 \
--dspark /path/to/Qwen3.8-27B-DSpark \
--tp-world 4 --devices 0,1,2,3 \
--lengths 512,8192,32768 \
--max-new-tokens 64
The focused prefix/cold-parity suite covers exact repeat, monotonic append, shorter prefix, interior branch, and compressed context under both target KV dtypes. With the default early 256-token snapshot spacing, a shorter/branched prompt may reuse the deepest safe 256-token boundary and recompute the remainder rather than claiming the entire matched prefix:
for dtype in fp16 fp8; do
/path/to/deepseek/bin/python scripts/bench_qwen_dspark_prefix_cache.py \
--ckpt /path/to/Qwen3.8-27B-FP8 \
--dspark /path/to/Qwen3.8-27B-DSpark \
--kv-cache-dtype "$dtype" \
--max-context 1024 --max-new-tokens 16
done
External DFlash2 speculative decoding¶
The external draft checkpoint is supported as an explicit opt-in:
The directory must contain its config.json and single model.safetensors. The runtime validates all 81 expected tensors and adds 1,450,191,360 sharded plus 3,848,808,960 replicated device bytes per rank. --qwen-dspark and --qwen-dflash2 are mutually exclusive, and neither can be combined with native MTP.
The checkpoint fixes block_size=8 with target_layer_ids = [5,19,33,47,61], a five-layer sliding-attention backbone (sliding_window=2048), selector_rank=256, and selector_top_k=16. Each transaction drafts up to seven tokens and verifies eight target rows. Partial rejection restores DeltaNet recurrent state and convolution tails, crops logical K/V to the committed position, and replays only the accepted prefix.
Unlike DSpark, DFlash2's residual and MLP down outputs exceed the FP16 range: a pure FP16 residual overflows in the first layer and produces NaN. The working SM75 mixed precision keeps the residual, down output, and finish convolution in FP32 while gate/up/SwiGLU stay in cuBLAS FP16, converting back to FP16 after each layer norm for attention and dynamic projection. All DFlash2 RMSNorms are standard direct-gamma, not the target's (1 + gamma).
Real TP4, full-model, FP16-target-KV A/B results with serial plain-then-DFlash2 execution and exact cross-mode plus all-rank token checks:
| Fixture | Plain wall | DFlash2 wall | Wall speedup | Decode speedup | Accept length | Parity |
|---|---|---|---|---|---|---|
| Synthetic 512, 512 new | 14.510 s | 5.228 s | 2.776x | 3.017x | 8 / 8 | PASS |
| Synthetic 4,096, 512 new | 21.019 s | 9.307 s | 2.259x | 3.104x | 8 / 8 | PASS |
| Synthetic 8,192, 512 new | 30.539 s | 15.677 s | 1.951x | 3.400x | 8 / 8 | PASS |
| GSM8K, 8 prompts, 256 new | 57.595 s | 43.356 s | 1.328x | 1.300x | 2.87 – 4.40 | PASS 8/8 |
Decode-phase speedup lands inside upstream's published 2.67–3.43x band on all three synthetic lengths. Upstream defines speedup as a per-token decoding latency ratio, so a full-request wall ratio is not the same metric: prefill is shared identically by both modes and caps the 8,192 case at 1.95x regardless of drafter quality.
GSM8K acceptance is far lower (0.55–0.71 per-draft rate) and per-prompt wall speedup tracks it directly, from 1.13x to 1.57x. Verify cost is roughly linear in block width because the 48 gated-delta layers recur sequentially over rows, so a full-width block pays for and discards the rejected tail. POCKETLLM_DFLASH2_ADAPTIVE_WIDTH=1 tracks an EWMA of accepted count and verifies ewma + 1.5 rows with a floor of two, which keeps the fully-accepted synthetic cases at width 8 while lifting the low-acceptance GSM8K case. A fixed width cannot serve both: at width 7 GSM8K regresses to 0.86x, and at width 2 the synthetic gains are discarded.
Four flags are opt-in and all four were enabled for the results above:
| Flag | Effect |
|---|---|
POCKETLLM_DFLASH2_CUBLAS_FP32=1 |
Routes the target LM head through cuBLAS FP32; the head is 55% of draft cost and this makes it ~10x faster |
POCKETLLM_DFLASH2_ADAPTIVE_WIDTH=1 |
EWMA-driven verify width, as above |
POCKETLLM_DFLASH2_SPLIT_TOPK=1 |
Partitions each row's local top-16 shard and merges with the identical comparator |
POCKETLLM_QWEN_GQA_OPTIMIZED=1 |
Selects the tiled GQA prefill kernel; long prefill is 64% full-attention, not FP8 GEMM |
POCKETLLM_DFLASH2_VITERBI_SELECTOR=1 computes an exact MAP over the selector chain instead of greedy argmax. It scores higher but accepts worse (3.02 to 2.95 accept length, 0.555 to 0.279 rate), so draft search is not an acceptance lever.
Reproduce the synthetic serial A/B with:
POCKETLLM_DFLASH2_CUBLAS_FP32=1 POCKETLLM_DFLASH2_ADAPTIVE_WIDTH=1 \
POCKETLLM_DFLASH2_SPLIT_TOPK=1 POCKETLLM_QWEN_GQA_OPTIMIZED=1 \
/path/to/deepseek/bin/python scripts/bench_qwen_dflash2.py \
--ckpt /path/to/Qwen3.8-27B-FP8 \
--dflash2 /path/to/Qwen3.8-27B-DFlash2 \
--lengths 512,4096,8192 \
--max-new-tokens 512 --prefill-chunk-tokens 4096 --snapshot-interval 0
And the dataset-shaped single-request workload, which matches upstream's aggregate-completion protocol rather than a fixed-token microbenchmark:
POCKETLLM_DFLASH2_CUBLAS_FP32=1 POCKETLLM_DFLASH2_ADAPTIVE_WIDTH=1 \
POCKETLLM_DFLASH2_SPLIT_TOPK=1 POCKETLLM_QWEN_GQA_OPTIMIZED=1 \
/path/to/deepseek/bin/python scripts/bench_qwen_dflash2_upstream.py \
--ckpt /path/to/Qwen3.8-27B-FP8 \
--dflash2 /path/to/Qwen3.8-27B-DFlash2 \
--dataset gsm8k --num-prompts 8 --max-new-tokens 256
Exact prefix reuse¶
QwenEngineOptions::prefix_cache is enabled by default for a long-lived engine. The full-attention KV cache is already indexed by absolute position, while the 48 DeltaNet layers carry a small recurrent state (state plus convolution tail). The engine retains both across sequential prefill requests and reports prefix_reused_tokens, prefix_computed_tokens, prefix_matched_tokens, and prefix_resume_source in the persistent stdin result.
The runtime stores device-resident recurrent snapshots at dense 256-token boundaries through the first 4K, then at the configured 4K interval through the 262K limit. This keeps a compressed or diverging request exact while limiting recomputation to the suffix after the selected snapshot. The default 82 snapshots use about 3.0 GiB per rank on the full 48-layer model; this is additional KV/state working memory and is included in the reported GPU memory. --qwen-no-prefix-cache, --qwen-snapshot-interval N, and --qwen-max-snapshots N provide explicit A/B controls.
A long-lived TP4 stdin worker can be started with --qwen-persistent-stdin --max-context N; rank 0 reads lines of the form <max_new_tokens> token0 token1 ..., and rank 1..3 receive the same requests over the Qwen command socket. This mode is intended for single-concurrency clients and preserves the cache between lines. Each request returns exact greedy tokens and prefix accounting. The ordinary one-shot CLI creates a fresh engine, so it cannot reuse a cache across processes and therefore keeps snapshots disabled; the reported one-shot long-context TPS and memory are unaffected by this feature. --qwen-no-prefix-cache fully disables snapshots, prompt history, and cached results even in persistent mode.
A real TP4 continuous-request test on the Qwen3.8-27B-FP8 checkpoint produced:
| Request | Prompt | Reused | Computed | Resume | Request prefill TPS |
|---|---|---|---|---|---|
| 1 | 512 | 0 | 512 | empty | 409.9 |
| 2 | 1,028 | 515 | 513 | live | 642.8 |
| 3 | 1,544 | 1,031 | 513 | live | 908.9 |
| 4 | 768 (256 common prefix + compressed suffix) | 256 | 512 | snapshot | 627.5 |
The cache-on and cold A/B runs generated identical tokens on all four TP ranks. In the cold run, requests ⅔/4 computed 1,028/1,544/768 tokens respectively, and every cold request reported prefix_snapshots=0 with prefix_snapshot_bytes=0. Prefix reuse is exact: it does not claim that newly compressed content is cached; only the unchanged token prefix is reused.
Native MTP was also exercised through this same long-lived protocol with adaptive K<=4, 64 generated tokens, two appends, and an interior compression branch. A separate serial plain run used the identical four requests. All generated sequences matched, every request had TP-rank parity, and both modes reported identical prefix accounting:
| Request | Prompt | Reused | Computed | Resume | MTP acceptance | Plain decode TPS | MTP decode TPS | Decode speedup | Plain wall | MTP wall | Wall speedup |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 512 | 0 | 512 | empty | 100.0% | 37.13 | 79.67 | 2.146x | 2.959 s | 2.219 s | 1.334x |
| 2 | 1,088 | 575 | 513 | live | 77.8% | 35.75 | 50.42 | 1.410x | 3.426 s | 3.120 s | 1.098x |
| 3 | 1,664 | 1,151 | 513 | live | 70.2% | 34.55 | 48.65 | 1.408x | 3.484 s | 3.169 s | 1.099x |
| 4 | 768 (256 common prefix + compressed suffix) | 256 | 512 | snapshot | 63.2% | 36.30 | 42.45 | 1.169x | 2.955 s | 2.897 s | 1.020x |
This validates the intended single-concurrency cache behavior and shows a wall win on all four requests, but it also confirms that the 1.5x requirement remains acceptance-dependent: only the 100%-acceptance request clears 1.5x decode and wall throughput. Appends reuse the target recurrent/KV state and the MTP shifted boundary; compression restores a target-hidden snapshot and rewrites the MTP boundary before priming the new suffix. The persistent harness reports wall, prefill, and decode timing separately; prefill includes MTP predictor priming when enabled.
The same harness was then run from a 32,768-token cold prompt with 512-token appends and a 4,096-token compression boundary. The run predates the current kernel defaults, so its absolute prefill figures are superseded by Validated performance; request 1 is that run's own cold baseline, so the reuse comparison holds within the table:
| Request | Prompt | Reused | Computed | Resume | Request prefill TPS | Snapshot bytes/rank |
|---|---|---|---|---|---|---|
| 1 | 32,768 | 0 | 32,768 | empty | 113.7 | 885,178,368 |
| 2 | 33,284 | 32,771 | 513 | live | 4,083.9 | 923,664,384 |
| 3 | 33,800 | 33,287 | 513 | live | 4,105.8 | 962,150,400 |
| 4 | 4,608 (4,096 common prefix + compressed suffix) | 4,096 | 512 | snapshot | 2,361.3 | 654,262,272 |
Request 1 matches the cold 32K prefill baseline. The two appends each execute only the 513 uncached tokens, and the compressed request recomputes only its 512-token suffix after restoring the 4,096-token snapshot. Snapshot memory shrinks when a shorter prompt invalidates later rollback points.
KV cache dtype and the 262,144-token boundary¶
FP16 KV cache is the default and the precision/performance baseline. FP8 E4M3 cache is an explicit
opt-in over 64-channel blocks that halves KV data per rank — 2,048 MiB instead of 4,096 MiB at the
262,140-token boundary — and is not the faster configuration at any length. The quantized-cache
comparison has its own document: ../performance/qwen_kv_cache_65k_tg512.md
measures FP16, FP8, TurboQuant K8V4 and INT8 per-token-head at 65,536 tokens with 512 generated
tokens and finds that dequant-once gives the quantized formats prefill parity with FP16 (within
0.3%) while decode falls to 0.124x (FP8), 0.370x (TurboQuant K8V4) and 0.052x (INT8 per-token-head).
Earlier revisions of this page said FP8 cache reduced prefill throughput as well; that measurement
predates dequant-once and is corrected here. The two cache dtypes also agree token for token: the
131,072-token FP16 and FP8 runs generated the same 128 tokens. FP16 greedy output is itself stable
across builds — ten runs of the 32,768-token case spanning five revisions and six runs of the
65,536-token case all produced the same sequence.
Chunked prefill is what makes the boundary reachable. It removes the previous prompt-length FP32
activation allocation, so the longest prompt the model accepts — 262,140 tokens, four positions short
of max_context=262144 — completes without OOM inside the 22 GiB/rank budget: 787.32 tok/s prefill
over 332.95 s, 4,096 MiB of KV data and no scale bytes per rank, a 15.24 GiB highest-rank
nvidia-smi peak, TP-rank parity, and [321, 5979, 13914, 13] as the four generated tokens. Of that
peak, 11.85 GiB is the engine's own accounting and the remaining 3.4 GiB is the same fixed
per-process overhead seen at every other prompt length.
The boundary case carries no decode column, and cannot: max_context has to cover the prompt plus
the generated positions, and the model is limited to 262,144 of them, so 262,140 prompt tokens leave
exactly four. --max-new-tokens 128 is refused at this length — all four ranks report Qwen max
context exceeds model configuration, with either cache dtype — so read the row as a
prefill-and-memory result only. An earlier revision of this page published 1.92 tok/s (FP16) and
0.64 tok/s (FP8) decode for it; those figures came from generations too short to measure decode and
are retracted. Decode is measured at 64, 512, 4,096, 8,192, 32,768 and 65,536 tokens in
Validated performance.
Decode Context Parallelism feasibility on four GPUs¶
Rejected for the current four-GPU topology. DCP can directly shard only the 16 full-GQA layers. The 48 Gated DeltaNet layers retain complete recurrent state on every replica and do not benefit from ordinary KV position sharding.
Keeping four GPUs constrains the proposed topology to TP2xDCP2. For a context length C, the per-GPU full-attention decode work is unchanged: TP4 performs 6 local Q heads over C positions, while TP2xDCP2 performs 12 local Q heads over C/2 positions. Both equal 6C head-position evaluations per device; the DCP topology then adds two DCP all-reduces per full-attention layer and doubles the local TP2 weights.
This was tested with a deliberately favorable upper bound: plain TP2 at half the TP4 context length, without the two DCP collectives or cache compaction. It was already slower and used substantially more memory. Both columns come from the same run, so the comparison stands, but the TP4 decode column was measured with 512-token prefill chunks on the kernels of the time and is superseded by Validated performance:
| TP4 context / result | TP2 half-context, no DCP communication | Result |
|---|---|---|
| 512 / 35.84 decode tok/s | 256 / 22.56 decode tok/s | 37% slower upper bound |
| 4,096 / 29.43 decode tok/s | 2,048 / 20.87 decode tok/s | 29% slower upper bound |
| 8,192 / 24.24 decode tok/s | 4,096 / 19.07 decode tok/s | 21% slower upper bound |
| 32,768 / 11.57 decode tok/s | 16,384 / OOM before prefill | infeasible |
TP2 local resident weights measured 13.72 GiB per rank, versus 6.86 GiB under TP4. Therefore an actual TP2xDCP2 implementation would be slower than these already-negative upper bounds and would introduce numerical/communicator complexity without reducing per-device attention work. The default TP4 path remains unchanged; no DCP code is enabled.
A useful context-parallel experiment requires at least eight ranks/GPUs for TP4xDCP2, which preserves the TP4 weight shard while halving per-device full-attention context. Even there, it would apply only to the 16 full-GQA layers and must beat the added two DCP collectives per such layer.
Per rank, the engine reported:
The stable pre-optimization decode baseline was approximately 22.4 tok/s. Rank-local full-attention K/V projection, grouped GQA value aggregation, and fused decode SwiGLU raised the measured result into the 35–37 tok/s range while preserving the separate prefill path.
Correctness and precision¶
- All four TP ranks generated identical token sequences on both the 64-token and 512-token fixtures.
- GQA decode matched the CPU reference with worst absolute error
1.192e-7. - Fused FP8 SwiGLU matched the separate projection path with
max_abs=0andmax_rel=0in its test fixture. - Qwen RMSNorm, gated RMSNorm, L2 normalization, online FP8 matvec/matmul, DeltaNet, convolution tail, GQA, and TP weight-sharding tests pass.
- Parallel GQA softmax changes reduction association. CPU-reference error remains near
1e-7, and real generated token sequences were unchanged in the validated runs. - The existing DeepSeek FP8 matvec/matmul and minimum-layer smoke tests were also run to protect the older path.
Reproduction¶
Build the C++ engine, then start four ranks with one shared NCCL ID file:
rm -f /tmp/pocketllm_qwen_nccl.id
for rank in 0 1 2 3; do
CUDA_VISIBLE_DEVICES=$rank \
build/cpp_engine/pocketllm_engine \
--ckpt /path/to/Qwen3.8-27B-FP8 \
--tp-world 4 --tp-rank $rank --device 0 \
--nccl-id-path /tmp/pocketllm_qwen_nccl.id \
--prompt "Explain tensor parallelism in one paragraph." \
--generate-token 123 --max-new-tokens 24 --smoke-layers 0 --resident-bench \
> /tmp/pocketllm_qwen_rank${rank}.log 2>&1 &
done
wait
The numeric value passed to --generate-token is ignored once --prompt supplies the prompt IDs; it currently activates the generation mode in the compatibility CLI parser. Rank 0 prints the timed result and all ranks print their local runtime/accounting lines. The CLI defaults to one smoke layer; use --smoke-layers 0 for a complete 64-layer performance claim.
For reproducible serial long-context TP4 measurements:
python scripts/bench_qwen_long_context.py \
--binary cpp_engine/build/pocketllm_engine \
--ckpt /path/to/Qwen3.8-27B-FP8 \
--tp-world 4 --devices 0,1,2,3 \
--lengths 64,512,4096,8192,32768,65536 \
--max-new-tokens 128 \
--prefill-chunk-tokens 8192 \
--kv-cache-dtype fp16 \
--layers 0 \
--tokenizer-python /path/to/deepseek/bin/python
This is the command behind Validated performance, including its generated-token budget and prefill chunk size; changing either makes the output incomparable to that table. The harness persists one log per rank, records rank-local timing and memory fields, checks greedy-token parity across TP ranks, and writes results.json after every successful context length. FP16-versus-FP8 cache parity is a separate comparison of the generated sequences from two serial runs.
The exact FP16 GQA path is on by default and is what those measurements exercise: it keeps full attention, uses a tiled prefill kernel, and uses compact split-context fused decode partials from context 4,096 onward on SM75 while shorter contexts retain the reference score/value decode path because it is faster there. On an earlier build, when the path was still opt-in behind POCKETLLM_QWEN_GQA_OPTIMIZED=1, it improved a 24-generated-token 32,768-token run from 97.75 to 159.52 tok/s prefill and from 10.66 to 17.36 tok/s decode, with token parity at every length. Those absolute values are superseded by Validated performance; the gate itself is unchanged: the direct CUDA test covers causal offsets through 333 tokens, head dimensions 64/256, contexts 4,096/8,192/32,768, and a 262,144-token compact-partial boundary check.
Sparse experiments require an explicit --qwen-attention-window N and may add --qwen-attention-sink-tokens N; N=0 is exact full attention. The sparse kernel attends to the leading sink prefix plus the newest window positions without changing KV-cache storage. This is an experimental semantic change, not an exact full-attention optimization claim. Window values that cover the complete context are directly checked against exact output; long-context quality and throughput are not reported here until measured on clean GPUs.
Audit only the rank-local weight mapping:
build/cpp_engine/pocketllm_engine \
--ckpt /path/to/Qwen3.8-27B-FP8 \
--tp-world 4 --tp-rank 0 \
--qwen-audit
Known limitations¶
- Text-only: no image/video preprocessing or vision-tower execution.
- Text-only serving: the native OpenAI-compatible server is validated for Qwen text requests, while the checkpoint's vision tower and multimodal request formats are not implemented.
- Stochastic sampling is available through
--temperature,--top-p, and--top-k. The default remains greedy (temperature 0) so existing benchmarks stay reproducible. Per-request sampling overrides are exposed in the batched API and the OpenAI server. - The model limit is 262,144 positions; with four generated tokens, the longest valid benchmark prompt is 262,140 tokens. That boundary is a prefill-and-memory result, not a throughput one — the position limit leaves no room for a decode measurement there. FP8 KV cache halves the boundary's KV footprint per rank by construction but is slower everywhere it has been measured; see
../performance/qwen_kv_cache_65k_tg512.md. - CUDA Graph and a decode megakernel remain future work; neither is included in the reported TPS.
- Native MTP is opt-in. Parity-safe high-acceptance cases accelerate decode by 1.67x at 4K, 2.47x at 8K, and 3.21x at 32K; 65–71% acceptance gives only 1.18–1.37x on 512-token varied prompts. Persistent exact-prefix workloads are the intended use case;
--qwen-mtp-adaptivestarts at K=1 and limits but does not eliminate low-acceptance overhead. - External Qwen DSpark is opt-in and always uses its fixed seven-draft/eight-row transaction. It accelerates high-draft-match decode by 1.61–1.92x in measured 512/8K/32K cases, while a bare greedy stress prompt achieved only 31/182 draft matches and regressed to about 17.4 tok/s. This draft-match ratio excludes bonus tokens and is not comparable to the model card's bonus-inclusive
spec_accept_length=3.39sampled-workload mean. Confidence is telemetry only; no unvalidated threshold is used to gate transactions. - Split exact-GQA verification is experimental and enabled only with
QWEN_GQA_VERIFY_SPLIT=1; direct numerical checks pass, but near-tie greedy output can drift. General long-prefill/decode optimized GQA and sparse attention remain separate opt-in paths; sparse attention changes model semantics.
Evidence and related notes¶
cpp_engine/include/qwen_config.hppcpp_engine/core/qwen_config.cppcpp_engine/engine/qwen_weights.cppcpp_engine/engine/qwen_engine.cppcpp_engine/backends/cuda/kernels/qwen_fp8_ops.cucpp_engine/backends/cuda/kernels/qwen_half_ops.cucpp_engine/backends/cuda/kernels/qwen_attention_ops.cucpp_engine/tests/test_qwen_config.cppcpp_engine/tests/test_qwen_fp8_online.cppcpp_engine/tests/test_qwen_gqa_attention.cppcpp_engine/tests/test_qwen_half_ops.cppcpp_engine/tests/test_qwen_weights.cppcpp_engine/tests/test_qwen_engine.cppcpp_engine/include/qwen_dspark.hppcpp_engine/engine/qwen_dspark.cppcpp_engine/backends/cuda/kernels/qwen_dspark_ops.cucpp_engine/tests/test_qwen_dspark.cppcpp_engine/tests/test_qwen_dspark_ops.cppscripts/bench_qwen_mtp.pyscripts/bench_qwen_dspark.pyscripts/bench_qwen_dspark_prefix_cache.py- Qwen3.8-27B-NVFP4 for the mixed NVFP4/FP8 checkpoint on the same text runtime
- Benchmark reporting rules