This file provides guidance to AI coding agents (Claude Code, Codex, Cursor, Copilot, etc.) working
with code in this repository. CLAUDE.md imports this file, so the same guidance applies to Claude Code.
Reproduction repository for the 3rd-place submission to the Efficient Qwen Competition (AdaptFM workshop, ICML 2026). Task: minimize Qwen3.5-4B inference latency on a single NVIDIA A10G (24 GB) while passing three quality gates — MMLU-Pro, IFEval, GPQA-Diamond.
Scope of work here is reproduction + source cleanup — a standalone, newcomer-runnable pipeline that produces the submission artifacts. The repo is fully self-contained. It is a frozen reproduction artifact, not an actively maintained project, and does not solicit external contributions — the goal is that a newcomer can re-run the pipeline, not extend it.
Root layout: scripts/ (entrypoints) + src/ (everything they run). Two src/ trees are
vendored upstream code — src/SpecForge/ (DFlash drafter-training framework; nested .git
removed, upstream HEAD 61f9cb0) and src/dflash_runtime/ (the dflash package: the DFlash
draft-model implementation vLLM imports at serve/eval time) — treat both as ordinary in-repo
source, edit directly. Models and generated artifacts are gitignored (data/, models/, runs/,
src/data_generator/*.jsonl, .venv*, .env, *.log, and SpecForge's own cache/ outputs/ wandb/).
Seven steps. src/data_generator/ is used twice (different teachers); the drafter is trained in
two phases (pre-train on 220K → fine-tune on 400K) before quantization:
1 data_generator (220K) → 2 qad → 3 drafter pre-train → 4 data_generator (400K) → 5 drafter fine-tune → 6 drafter quant → 7 serve+eval
teacher=Qwen3.5-4B QAD INT4 (CT) scratch on 220K teacher=QAD ckpt5000 warm-start on 400K GPTQ-W4A16 W4 target + W4 draft, SWA 1024
src/data_generator/(220K) — rebuild the fixed 220K prompt set from the shipped uuid manifest (src/data_generator/manifests/, viarebuild_prompts_from_manifest.py— NOTsample_nemotron.py, which can't reproduce this draw), serve the BF16 teacherQwen/Qwen3.5-4B, regenerate responses →data/qwen3_5_nemotron_combined_regen.jsonl(~220K convs).src/qad/— Quantization-Aware Distillation. Loadcyankiwi/Qwen3.5-4B-AWQ-4bit, preserve its INT4 weight grid exactly by injecting its per-group scales as modelopt_amax, then distill the BF16-latent weights against the BF16 teacher on the 220K data with per-token KL. Deploys as INT4-weight / FP16-act (compressed-tensors) with zero re-quant noise. Final target = ckpt5000.- drafter pre-train (
scripts/train_drafter_pretrain.sh) — DFlash draft from scratch on the 220K convs, target = BF16 baseQwen/Qwen3.5-4B(the 220K teacher; no QAD dependency, so this can run in parallel with step 2) →src/SpecForge/outputs/dflash-pretrain. src/data_generator/(400K) — re-run the generator with the QAD model as teacher →src/data_generator/regen_ckpt5000.jsonl(~400K convs). The draft must predict the QAD-applied model's outputs.- drafter fine-tune (
scripts/train_drafter_finetune.sh) — warm-start the best pre-train ckpt, train on the 400K set →src/SpecForge/outputs/dflash-finetune. - drafter quant (
scripts/quant_dflash_gptq.sh+src/drafter_quant/) — GPTQ-W4A16 the fine-tuned draft → compressed-tensors pack (g128 sym INT4). Near-lossless vs the BF16 draft. On vLLM 0.22.1 the W4A16 target forces the draft to be quantized too, so this is required, not optional. src/serving/+src/eval_harness/— FastAPI proxy that spawnsvllm serve(QAD target + DFlash draft asspeculative_config), served with the drafter plugin:EQC_DFLASH_QUANT_PATCH=1(W4 draft) +EQC_DFLASH_SWA_WINDOW=1024(sliding-window attention). HTTP eval harness for the three gates + latency.
One venv for the whole repo — a single environment defined by pyproject.toml (+ uv.lock).
Build from the repo root:
uv sync # runtime: serve + data-gen + DFlash-eval + eval gates
uv sync --all-extras # + QAD train/pack (`qad`) + DFlash train (`train`) deps
uv pip install -e src/SpecForge --no-deps # SpecForge training framework (its deps are in `train`)Single resolution: torch 2.11.0+cu130 / transformers 5.5.4 / triton 3.6 / vLLM 0.22.1 /
nvidia-modelopt 0.44. Why one venv is possible: (1) sglang is excluded — it hard-pins
transformers==4.57.1 (clashes with Qwen3.5's 5.x); SpecForge trains fine on its hf backend
without it. (2) modelopt 0.44 caps transformers to ~5.5 and vLLM 0.22.1 accepts 5.5.4, so both
share transformers==5.5.4. (3) lm-eval's [math] extra is dropped (unused by the gates; antlr pin
clash). .env (gitignored) holds HF_TOKEN / WNB_TOKEN / HF_HOME; the train scripts auto-source it.
HF_TOKEN is required (not optional): nvidia/Nemotron-Post-Training-Dataset-v2 (step 1 prompts)
and Idavidrein/gpqa (GPQA-Diamond gate) are gated — accept each dataset's terms on HF first. The
models and the other eval sets are public.
| stage | dir | notable deps | installed by |
|---|---|---|---|
| serving | src/serving/ |
vLLM 0.22.1, fastapi/uvicorn, eqc_vllm_plugin (editable) |
base (uv sync) |
| data gen | src/data_generator/ |
vLLM 0.22.1, openai | base |
| DFlash eval | — | vLLM 0.22.1 + dflash (editable) |
base |
| quality/latency eval | src/eval_harness/ |
lm-eval 0.4.11 (HTTP client) | base |
| QAD train + pack | src/qad/ |
modelopt 0.44, deepspeed, fla, causal-conv1d, bitsandbytes | [qad] extra |
| DFlash train | src/SpecForge/ |
HF backend, no sglang | [train] extra + -e src/SpecForge |
# --- Setup ---
uv sync --all-extras && uv pip install -e src/SpecForge --no-deps
# --- Step 2: QAD (train + pack) ---
scripts/prepare_data.sh # tokenize + pack (CPU, seq 16384)
scripts/train_multi.sh 0,1,2,3,4,5,6,7 src/configs/train_config_smoke.yaml # 50-step smoke (needs ≥2 GPUs)
scripts/train_multi.sh 0,1,2,3,4,5,6,7 src/configs/train_config.yaml # full 8000 steps (ZeRO-2, ≥2 GPUs)
scripts/pack_ct.sh checkpoint-5000 # → CT pack + dense -bf16 twin
# --- Steps 3/5/6: DFlash drafter (pre-train → fine-tune → quant) ---
scripts/train_drafter_pretrain.sh 8 flex_attention # scratch on 220K → src/SpecForge/outputs/dflash-pretrain
# edit INIT_CKPT (best pre-train ckpt) in the finetune script, then:
scripts/train_drafter_finetune.sh 8 flex_attention # warm-start on 400K → src/SpecForge/outputs/dflash-finetune
.venv/bin/python src/drafter_quant/build_calib.py --source src/data_generator/regen_ckpt5000.jsonl \
--out runs/drafter_quant/calib --total 256
scripts/quant_dflash_gptq.sh # GPTQ-W4A16 → runs/drafter_quant/dflash_w4_gptq
# --- Step 7: serve + eval (single-GPU = A10G latency-faithful) ---
export PATH="$PWD/.venv/bin:$PATH" # so the spawned `vllm` CLI resolves
# latency path: QAD target + W4 draft + SWA 1024
CUDA_VISIBLE_DEVICES=0 PYTHONPATH=src EQC_CONFIG_PATH=src/configs/serve_dflash_gptq.yaml \
EQC_PORT=8080 EQC_VLLM_INTERNAL_PORT=28080 EQC_GPU_MEM_CAP_FRACTION=0.30 \
EQC_DFLASH_QUANT_PATCH=1 EQC_DFLASH_SWA_WINDOW=1024 \
.venv/bin/python -m serving # wait: curl localhost:8080/ping
# quality gates (dev sample, then full) — harness is HTTP-only, start server first:
QUALITY_LIMIT=0.1 CONTAINER_URL=http://localhost:8080 .venv/bin/python src/eval_harness/run_quality_local.py
CONTAINER_URL=http://localhost:8080 .venv/bin/python src/eval_harness/run_latency_harness.pyDeployment target is one A10G (24 GB), but training/serving happens on A100/H100 80 GB boxes.
scripts/gpu_lock.sh locks one GPU and sets EQC_GPU_MEM_CAP_FRACTION=0.30 (≈24/80) to emulate the
A10G cap. Latency must be measured on a single GPU with this cap; quality eval is throughput
and may run data-parallel (EQC_VLLM_DP_SIZE). Tensor-parallel is out of scope. QAD training shards
with DeepSpeed ZeRO-2 because seq-16384 full-vocab (248K) logits don't fit one 80 GB card — ≥2 GPUs
required (a 1-GPU run OOMs at the first step); effective batch (16) is preserved (grad_accum ÷ world_size).
- triton 3.6 + tilelang on Hopper: the unified venv ships
triton==3.6(torch 2.11 / vLLM 0.22.1) plustilelang==0.1.9(a vLLM 0.22.1 dependency, souv syncalways installs it). On Hopper (sm_90) QAD's fla gated backward JIT-compiles via tilelang under triton ≥3.4 — so re-training QAD from scratch on H100 works in the unified venv, no isolatedtriton==3.3.1env needed. - QAD packing: use
src/qad/pack_bf16_to_ct.py(manual quant from BF16 latent + injected_amax), not modeloptmtq.compress— the latter re-quantizes off-grid and adds KL.scripts/pack_ct.sh checkpoint-Nresolves the ckpt underckpts_full/(orckpts_smoke/) and writes two dirs: the CT packlocal_model_ct_ckptN(serving) and the dense dequantized twinlocal_model_ct_ckptN-bf16(drafter fine-tune / GPTQ-calib target). - Data gen: one independent DP=1 vLLM server per GPU (not
--data-parallel-size N, which deadlocks);--keep-truncatedis the biggest throughput lever;/healthlies during warmup (probe with a real generation); spec-decode garbles W4A16 checkpoints soSPEC=offby default. The 220K step-1 corpus is rebuilt from a uuid manifest (it drew from the multilingual splits and usedid = md5(user text); the current sampler is non-ML-only withid = uuid— the two id schemes must not be mixed, e.g. in--exclude-ids-file). Seesrc/data_generator/README.md. - DFlash training:
--dataloader-num-workers 0is mandatory (fork-CUDA deadlock); micro-batch 2 on H100 80 GB (vocab-248K logits OOM higher); both drafter scripts auto-deriveaccumto keep effective batch 32; sglang must stay uninstalled. Patches are insrc/SpecForge/dflash_hf.patch. Pre-train target = BF16 baseQwen/Qwen3.5-4B; fine-tune/GPTQ-calib target = dequantized bf16 (local_model_ct_ckpt5000-bf16, written bypack_ct.sh); eval/serve target = W4A16 original. - Serving: FastAPI proxy spawning
vllm serve(offlinevllm.LLMdeadlocks under the harness); it invokes thevllmCLI by name, so put.venv/binonPATH, and theservingpackage is run in place fromsrc/(not installed), so setPYTHONPATH=src./invocationsis polymorphic (prompt→completions, messages→chat); thinking defaults OFF except GPQA. SetEQC_VLLM_INTERNAL_PORT=28080(8080+10000 collides with the harness proxy). Seesrc/serving/README.md. - Eval datasets must be pre-fetched into
HF_HOMEonce (harness runsHF_HUB_OFFLINE=1); GPQA (Idavidrein/gpqa) is gated, so the fetch needsHF_TOKEN. Snippet in top-level README. PYTHONPATH=srcshadowing: a directory directly undersrc/must not share its name with an installed import package — it would resolve as an empty namespace package and shadow the real one. That's why the editable-package project dirs differ from their import names:src/vllm_plugin/holdseqc_vllm_plugin,src/dflash_runtime/holdsdflash.
src/configs/—train_config.yaml(full QAD),train_config_smoke.yaml,ds_zero2.json,serve_config.yaml(QAD target only),serve_dflash_gptq.yaml(target + W4 draft, latency path).scripts/—prepare_data·train_multi·pack_ct(QAD),train_drafter_pretrain·train_drafter_finetune(drafter),quant_dflash_gptq,gpu_lock.src/dflash_runtime/+src/vllm_plugin/— editable packages installed byuv sync([tool.uv.sources]): the DFlash draft-model implementation and the W4-draft/SWA vLLM plugin.runs/(gitignored) — generated artifacts:qad_nemotron_regen/ckpts_full/checkpoint-*, packed CT modelslocal_model_ct_ckpt*,drafter_quant/dflash_w4_gptq.src/templates/qwen_no_think.jinja— chat template that disables thinking.- Per-stage runbooks live in
src/data_generator/README.md,src/serving/README.md,src/eval_harness/README.md; the top-levelREADME.mdis the newcomer entry point.