Skip to main content

Qwen3.8-27B and native MTP speculative decoding

Run from the repository root on a checkout containing MTP support (after the 0.6.0 release). This recipe uses the model's native multi-token prediction head to draft tokens, then verifies a chunk with the main model. No separate draft model is needed. Qwen3.8 is loaded through the vision-language architecture: its MTP heads support text inference, but auxiliary MTP training loss is not implemented for that architecture. The checkpoint-compatibility training warning does not mean its inference heads are missing.

Hardware and checkpoint​

An RTX 5090 has 32 GB VRAM. Full BF16 27B weights exceed that capacity; use a compatible INT4 checkpoint with its MTP weights retained, or multiple GPUs for an appropriately configured BF16 deployment. This example is single-GPU; it does not establish MTP support for tensor-parallel inference.

Install Eole in a CUDA-enabled environment with the CUDA toolkit and build tools from the installation guide. For this configuration install FlashAttention and the hybrid-attention kernels used by Qwen:

MAX_JOBS=2 python -m pip install -e . --no-build-isolation
MAX_JOBS=2 python -m pip install flash-attn --no-build-isolation
python -m pip install fla-core
MAX_JOBS=2 python -m pip install causal-conv1d --no-build-isolation

INT4 AutoRound/GPTQ checkpoints require a supported packing layout and Eole's Marlin CUDA extension. Build/install Eole while CUDA and the CUDA toolkit are available; inspect backend diagnostics before measuring. Installing a package does not prove its kernels are selected.

The recommended INT4 checkpoint for this recipe is RedHatAI/Qwen3.8-27B-INT4. Prepare it from the repository root in your Eole environment:

export EOLE_MODEL_DIR=/path/to/models
export QWEN38_MODEL="$EOLE_MODEL_DIR/Qwen3.8-27B-INT4"
python eole/bin/main.py convert HF \
--model_dir RedHatAI/Qwen3.8-27B-INT4 \
--output "$QWEN38_MODEL" \
--dtype bf16 \
--check-tensors

Use a new output directory for conversion. Add --token "$HF_TOKEN" if Hugging Face authentication is needed. The converter accepts a repository ID or a local HF model directory; it converts existing INT4 weights rather than quantizing BF16 weights. Source weight downloads stay in the HF cache.

If you already have the converted RedHat checkpoint, set the two path variables above to its location. Prediction, serving, MTP, and validation YAMLs all use QWEN38_MODEL; they do not need separate model-path edits.

Compressed-tensors INT4 layout​

RedHat uses symmetric grouped W4A16 pack-quantized weights.

The converter transposes packed INT4 words and scales into Eole's GPTQ layout; it does not dequantize and re-quantize the weights. Inference uses the existing AutoRound/GPTQ backend selection, including Marlin when available. Exact module paths from the checkpoint preserve unquantized projections, vision weights, and MTP heads. GDN kernels and normalization are unchanged.

Supported exports have one Linear weight-only configuration group, symmetric 4-bit integer weights, and group size 32, 64, or 128. Static activation ordering without saved group indices is accepted. Asymmetric weights, activation quantization, reordered groups, transformed/sparse exports, and packed weights requiring sliced or unmapped module transformations are rejected.

FP8 KV-cache scales in the source are not used: conversion logs a warning and Eole retains its floating-point KV cache. --check-tensors may list those cache scale tensors as unused. This is weight-format support, not FP8-cache support. CPU tests verify packed-value equivalence, sharded conversion, and mixed-precision module selection. The converted RedHat checkpoint has been exercised with compiled, MTP-disabled GPU inference; the targeted comparison below records the results. RedHat MTP throughput and greedy parity have not been measured.

For a BF16 deployment with sufficient memory, use Qwen/Qwen3.8-27B as the source and a separate output directory. Quantized checkpoints are not all equivalent: some omit MTP tensors. Conversion cannot recreate missing trained heads. Check the converted artifact before loading:

python recipes/qwen38/check_checkpoint.py "$QWEN38_MODEL"

The checker requires decoder num_mtp_heads > 0, saved mtp_heads.* tensors, and a chat template. It prints tensor names and quantization metadata without loading weights onto the GPU. A successful check establishes artifact contents, not GPU kernel compatibility or model quality.

Serve with MTP​

eole serve -c recipes/qwen38/serve.yaml --host 127.0.0.1

top_k: 1 deliberately selects greedy decoding, including for API clients that send a nonzero temperature. This recipe does not use sampling. It sets one sequence/hypothesis and disables prefix caching/chunked prefill to provide a simple baseline. Start with the conservative 32K context and lower it if VRAM is tight; the model's advertised maximum context is not a VRAM guarantee.

curl --fail-with-body http://127.0.0.1:5000/v1/messages \
-H 'Content-Type: application/json' \
-d '{"model":"qwen3.8-27B","max_tokens":256,"messages":[{"role":"user","content":"Write a short Python function that reverses a string."}]}'

Look for MTP draft acceptance in server logs. A fallback warning means the request used ordinary decoding; merely setting the flag does not demonstrate speculation. Image inputs, batches larger than one, beam search, sampling, forced prefixes, and unsupported decoding constraints currently fall back.

For a real coding client continue with Claude Code.

Compare against ordinary greedy decoding​

Stop the server first so only one process owns the checkpoint. The two inference YAML files use the same model, prompt, precision, and decode settings; only MTP and the output filename differ. The prompt fixture is one preformatted Qwen chat per line, with Eole's ⦅newline⦆ marker for embedded newlines.

eole predict -c recipes/qwen38/predict.yaml
eole predict -c recipes/qwen38/predict-mtp.yaml

These save /tmp/qwen38-baseline.txt and /tmp/qwen38-mtp.txt. Compare the text and inspect report_time / draft-acceptance logs. For repeated warm measurements, the benchmark reads those same inference YAML files:

Run all four configurations explicitly, so inherited environment variables do not change the comparison. Stop other GPU model workloads first. From the repository root, with QWEN38_MODEL set:

BENCH_DIR="/tmp/eole-qwen38-bench-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$BENCH_DIR"
nvidia-smi > "$BENCH_DIR/gpu.txt"
git rev-parse HEAD > "$BENCH_DIR/commit.txt"

for COMPILE in 0 1; do
if [ "$COMPILE" = 0 ]; then MODE=eager; else MODE=compiled; fi
for PATH_MODE in baseline mtp; do
if [ "$PATH_MODE" = baseline ]; then
CONFIG=recipes/qwen38/predict.yaml
else
CONFIG=recipes/qwen38/predict-mtp.yaml
fi
EOLE_TORCH_COMPILE="$COMPILE" EOLE_COMPILE_MODE=0 \
python recipes/qwen38/benchmark.py -c "$CONFIG" --runs 5 \
--output "$BENCH_DIR/$MODE-$PATH_MODE.json" \
> "$BENCH_DIR/$MODE-$PATH_MODE.log" 2>&1 || exit 1
done
done
echo "$BENCH_DIR"

Each invocation loads one engine, excludes one warmup, and measures five runs of the same prompt. Compile mode 0 requests CUDA graphs; backend diagnostics do not prove graph capture. Check measured runs for remaining warmup or timing outliers. Outputs include wall time, peak allocated VRAM, decoded text, and approximate retokenized output counts. Use Eole's logs for internal generated counts, decode timing, and acceptance; retokenization can change token counts. EOS can end a completion early, so verify equal generated lengths. Request wall time includes prefill, decoding, and other overhead. report_time also enables MTP GPU phase profiling, whose instrumentation is included in these timings. The harness fails if an MTP run reports fallback or never logs draft acceptance.

Record GPU/driver, PyTorch/kernel versions, checkpoint ID, precision, prompt/output lengths, compile mode, and draft acceptance with shared results. Try self_speculative_num_tokens: 1, 2, and 3 in a local YAML copy; more drafts can be slower when rejected.

Run the complete local validation​

validation.yaml names the checkpoint, baseline/MTP inference YAML files, server YAML, loopback port, output directory, and optional Claude Code check. Set QWEN38_MODEL as above or edit model_path in a local copy. Run from the repository root in a terminal that can access the GPU:

python recipes/qwen38/run_validation.py -c recipes/qwen38/validation.yaml

This runs eager baseline/MTP benchmarks sequentially, starts the configured loopback server, runs the API smoke checks, and optionally asks Claude Code to read a sentinel file in an isolated directory (claude: true). It stops only the server it started. Logs, JSON results, and a SUCCESS marker are saved in the configured output directory. A failure preserves logs and exits nonzero. Set claude: false for API-only checks. Use a fresh output directory per run. Inspect comparison.json, MTP acceptance in server.log, and the Claude Read tool block before calling the integration validated. The sentinel exercise does not establish a complete file-editing workflow; use the Claude recipe's coding task for that additional check.

Targeted INT4 comparison (2026-10-08)​

The converted RedHat checkpoint passed two of four medium LiveCodeBench tasks that the corrected-RoPE Frozenlock AutoRound checkpoint failed. Both models were rerun on the same Eole code with identical prompts, BF16 compute, compiled inference, MTP disabled, greedy decoding, and an 8192-token output budget. Neither model reached that budget on these tasks.

TaskFrozenlock AutoRoundRedHat INT4
atcoder/abc392_dFailFail
atcoder/abc396_dFailPass
leetcode/3779FailFail
leetcode/3722FailPass

This motivates the RedHat default, but the tasks were deliberately selected from AutoRound failures; 2/4 is not an overall LiveCodeBench score. See the comparison record. Run this recipe's MTP validation on your own hardware before using the historical throughput figures as a guide.

Historical Frozenlock RTX 5090 example (2026-10-07)​

These measurements use the previous Frozenlock default, not RedHat. The recipe's prompt fixture was run at commit ff2ab2eb3edfe5c09e449284b62c16f050973081 with the local converted Frozenlock/Qwen3.8-27B-int4-Autoround checkpoint, INT4 weights / BF16 compute, PyTorch 2.12.1+cu132, FlashAttention 2.8.3, FLA 0.5.2, and Triton 3.7.1. The driver was 610.43.02 and the pre-run power limit was 575 W; sustained power and clocks were not recorded. One warmup was excluded per configuration; each of five measured runs generated 256 tokens with batch size one. Compiled runs used mode 0; CUDA graph capture was requested but not verified.

ConfigurationMedian request wall timeMedian reported decode throughputEnd-to-end throughputMaximum peak allocated GPU memory
Eager, ordinary greedy11.12 s23.3 tokens/s23.0 tokens/s19.25 GB
Eager, MTP (three drafts)3.85 s69.2 tokens/s66.4 tokens/s19.65 GB
Compiled, ordinary greedy4.24 s63.0 tokens/s60.3 tokens/s23.59 GB
Compiled, MTP (three drafts)2.64 s115.1 tokens/s97.0 tokens/s23.92 GB

End-to-end throughput is 256 divided by median request wall time; reported decode throughput comes from Eole's internal timing and excludes prefill and some request overhead. Memory is PyTorch peak allocated memory in decimal GB, not total process VRAM. Medians include all five runs. Compiled baseline run 2 was slower (5.22 s versus 4.20–4.26 s for the others); it was retained. Compiled MTP wall time ranged from 2.636 to 2.648 s.

Compare MTP against the baseline with the same compilation setting. Compilation alone brings baseline decode throughput from 23.3 to 63.6 tokens/s. Adding MTP to the compiled baseline gives about 1.81× reported decode throughput and 1.61× end-to-end throughput on this fixture. In eager mode, the corresponding gains are 2.97× and 2.89×. The eager comparison alone does not describe the benefit over compiled ordinary decoding.

MTP accepted 186/204 drafts (91.2%) in every eager run and 184/211 (87.2%) in every compiled run. Text was repeatable within each configuration, but MTP and baseline outputs differed in both modes. These fixed-length timings do not establish exact greedy parity or complete/correct generated code. Use longer output limits for coding tasks and validate the resulting files.

The recorded measurements preserve all five timings, decode rates, output hashes, backend diagnostics, and comparison limits. This is one short prompt, not a general ranking across workloads or engines.

34 CPU fallback tests exercise native MTP and greedy verification; those tests alone do not certify GPU behavior. The live API run confirmed text generation and active MTP, and exposed XML numeric tool arguments being returned as strings. That server conversion bug has a schema-aware fix and regression tests. The live text, numeric tool round-trip, text SSE, and tool SSE checks passed after the fix. Claude Code 2.1.79 passed Read, Write, and Bash checks, including a generated three-test suite. Its configured output budget was 2048 tokens. See the implementation review for rounding and backend limitations.

Quantization module selection uses the existing quant_layers and quant_exclude_modules settings. Bare include names such as down_proj select that leaf name anywhere in the model. Dotted entries select exact paths from the model root, and shell-style globs such as decoder.transformer_layers.*.mlp.down_proj select decoder projections only. * can span path components. Exclusions take precedence; excluding a parent skips its entire subtree. Bare exclusions retain their legacy behavior of matching parent names anywhere in the model.

For compressed-tensors conversion, quant_layers is populated with exact Eole paths from the packed weight inventory, preserving floating-point decoder and MTP modules. Loading rejects selections that conflict with stored packed weights. All quantization backends share these matching rules. Bits and group size remain global settings; per-module quantization parameters are not supported.