inference-quantize-calibrate
Fast model loading (always-applied): when staging a base checkpoint or standing up a vLLM deploy, never load 100s-of-GB single-stream via s3fs FUSE (a GLM-5.1-sized model takes ~50 min that way. See docs/METHODOLOGY.md). Prefer, in order: a fast model-loading endpoint when reachable -> parallel multipart to local NVMe (server/tools/stage-model-parallel.py) -> runai_model_streamer (--load-format runai_streamer) -> tensorizer. Flag a slow load loudly: if effective rate < ~500 MB/s on a large model, STOP and switch. Details: server/docs/inference-fast-model-loading.md.
Purpose
Produce a quantized checkpoint from a base model -- rather than relying on
NVFP4 weights pulled pre-quantized from nvidia/*-NVFP4 HF repos. This skill
provides the calibrate + quantize recipe for a fresh model as a
two-Job PTQ pipeline:
- Calibration-dataset prep Job -- tokenizes a small calibration corpus
(default: a few hundred samples of C4 / the model's chat template) into the
format the quantizer's PTQ calibration loop expects.
- Quantize Job -- runs the quantizer over the base weights with the
calibration set, emitting quantized weights + per-layer activation/KV scales.
Two pluggable backends:
| Backend |
Target |
Use when |
NVIDIA ModelOpt (modelopt, default) |
NVFP4 |
Matches the nvidia/*-NVFP4 serving checkpoints (modelopt 0.45, quant_algo=NVFP4, group_size 16). |
| llm-compressor |
FP8 (compressed-tensors) |
FP8 weight quant for an FP8-tier deploy or an FP8-vs-NVFP4 A/B. |
The output PVC is experiment-prefixed and the result feeds straight into a deploy
When to use
- A new model has no published NVFP4/FP8 variant and you need quantized weights
to serve it on Blackwell efficiently.
- You want to reproduce / improve on a vendor NVFP4 checkpoint with your own
calibration set (e.g. a domain-specific calib corpus).
- The
inference-model-optimize orchestrator reached Phase 4.
Do not use this skill for:
- Models that already have a good vendor NVFP4/FP8 checkpoint you trust -- just
pull the vendor checkpoint (cheaper, byte-for-byte vendor recipe).
- KV-cache dtype selection at serve time (
--kv-cache-dtype=fp8_e4m3) -- that is
a vllm.extraArgs lever, not weight PTQ. Tune it with
inference-tune-sweep.
- Runtime activation-quant kernel work --
that is custom-kernel territory, not this PTQ pipeline.
Example prompts
- "Quantize MiniMax-M2.7 to NVFP4 with modelopt and validate accuracy."
- "Run an FP8 llm-compressor PTQ on this model and A/B it against BF16."
- "Produce NVFP4 weights for
zai-org/GLM-5.2 with a C4 calibration set."
/inference-quantize-calibrate --base /models/base --backend modelopt --target nvfp4
Prerequisites
The skill fails closed if any of these are not satisfied.
- The base (BF16 or FP8) checkpoint reachable (HF id / S3) -- you quantize
from the base, not the NVFP4 serving copy. On the current GB300 (4 GPU/node)
a large MoE base may need TP across 2 NVLink-domain nodes. B200 fits 8/node.
- Output persistence (cluster-profile aware): on plain-K8s (no storageclass)
write to emptyDir + upload to S3-compatible object storage from inside the
pod. On legacy Slurm-on-K8s, an experiment-prefixed PVC
(never reuse a standing
*-cache PVC).
- Cluster access + a free GPU node: plain-K8s GB300 (default) via
kubectl GPU requests. Legacy Slurm-on-K8s via sinfo/squeue. The quantize Job is
GPU-resident for calibration forward passes.
- The quantizer container image (modelopt or llm-compressor) -- the
quantize/
templates pin one. Air-gapped clusters mirror it first.
- The explicit ack flag for the quantize Job (it consumes a GPU node).
Workflow
Phase 0: confirm intent + scaffold the quant bundle
State back: base path, backend (modelopt | llm-compressor), target
(nvfp4 | fp8), calibration corpus + sample count, output PVC name
(experiment-prefixed), and the experiment-id (== the parent run-id). Read the
base config.json for arch + chat template so calibration uses the right
formatting. The scaffolder lays down quantize/:
quantize/00-calib-pvc.yaml -- experiment-prefixed output PVC.
quantize/01-calib-dataset-job.yaml -- calibration-dataset prep Job.
quantize/02-quantize-job.yaml -- the PTQ quantize Job (backend-parameterized).
quantize/quantize.py -- the backend dispatcher (modelopt / llm-compressor).
quantize/README.md -- the per-bundle runbook.
Phase 1: prepare the calibration set
Apply 01-calib-dataset-job.yaml. It tokenizes the calibration corpus with the
model's tokenizer + chat template into the quantizer's expected JSONL/arrow and
writes it to the output PVC. Gate: Job Complete + the calib file exists and has
the requested sample count.
Phase 2: run the quantize Job (ack-gated)
# render the manifest and verify that both lines equal <id-slug>:
kubectl -n <slurm-namespace> apply --dry-run=server \
-f quantize/02-quantize-job.yaml \
-o jsonpath='{.metadata.labels.experiment}{"\n"}{.spec.template.metadata.labels.experiment}{"\n"}'
# then apply (the Job carries the ack/intent in an env flag the entrypoint checks):
kubectl -n <slurm-namespace> apply -f quantize/02-quantize-job.yaml
The Job runs quantize.py:
- modelopt path:
mtq.quantize(model, NVFP4_DEFAULT_CFG, calib_loop) then
export to a HF-layout NVFP4 checkpoint (quant_algo=NVFP4, group_size 16) +
hf_quant_config.json, so vLLM auto-detects it exactly like a
nvidia/*-NVFP4 repo.
- llm-compressor path: a
oneshot FP8 recipe (compressed-tensors), emitting
per-layer scales.
Outputs land at /models/quantized/target on the experiment PVC. Gate: Job
Complete + the output config.json's quant method is the expected one
(modelopt/modelopt_fp4/NVFP4 for NVFP4, compressed-tensors/fp8 for FP8)
-- the same assertion the 03b puller makes on a vendor checkpoint.
Phase 3: validate the quantized weights (the gate that matters)
Deploy the quantized weights (the parent orchestrator's Phase 5, or a standalone
deploy off the experiment PVC) and run:
Gate: accuracy delta within tolerance (default: no metric drops > 1.0 absolute
point) AND a perf win. A quant that wins perf but fails accuracy is a
no-ship -- report the regression and keep the base/vendor checkpoint. Record
the accuracy + perf result as a baseline so future re-quants diff against it.
Phase 3b: 3-way comparison vs the vendor NVFP4 (our-PTQ vs NVIDIA-PTQ)
When a vendor nvidia/*-NVFP4 (or *-FP8) checkpoint exists for the same model,
"is our PTQ good enough" is the wrong question -- the decision is which of the
two NVFP4 checkpoints to ship. Run a head-to-head 3-way comparison across
three variants on one pinned node:
| Variant |
Source |
Role |
base |
BF16/FP8 base |
accuracy ceiling reference |
ours |
our ModelOpt NVFP4 (Phase 2 output) |
candidate |
nvidia |
nvidia/*-NVFP4 (pulled from HF) |
incumbent baseline |
Compare on three axes, each artifact-cited:
- Accuracy --
inference-model-eval
(GPQA + MMLU-Pro) for all three. Report ours and nvidia as deltas vs
base.
- Perf -- median TPOT + throughput, same-node + >=3 trials
(
inference-tune-sweep controlled A/B).
- Per-kernel --
inference-kernel-ncu-profile
on the dominant decode kernel of each, to explain why one wins (tensor-core
engagement, achieved roofline %) rather than asserting it.
Implement as a perftunereport campaign with one cell per variant (base /
ours / nvidia) so the three land side-by-side in the atlas + PDF + perf-lake
automatically, and the verdict ("ship ours" / "ship vendor" / "tie -> keep
vendor") is a published, data-backed row -- not a claim. A common honest outcome
is parity, in which case the vendor checkpoint wins on provenance. Record that.
Phase 3c: NVFP4-MoE serving gotchas
A quantized MoE checkpoint can fail to serve (or serve garbage) for reasons
unrelated to PTQ quality. Three gotchas, each with the fix:
- Activation-capability gap. The native FP4 MoE kernels (CUTLASS,
FlashInfer trtllm-gen) only advertise a fixed activation set. A model whose
expert FFN uses an unlisted activation (gemma4 =
gelu_pytorch_tanh ->
MoEActivation.GELU_TANH) is rejected at engine init even though the weights
are fine. Fix: add the activation to the kernel class _supports_activation
(CUTLASS routes non-SiLU through the generic apply_moe_activation, so it is
often a Python-only add) + the FlashInfer ACTIVATION_TO_FI_ACTIVATION map
(note: FlashInfer has no tanh-gelu variant -> trtllm maps GELU_TANH -> Geglu,
an approximation. The exact path is CUTLASS).
- Intermediate alignment is TP-dependent. The FP4 MoE kernels need the
per-rank expert intermediate
%64 (and the fused gate+up 2N %128 for
trtllm). moe_intermediate_size that is fine at TP=1 can break at TP=4
(gemma4: 704 -> per-rank 176, not %64) with a NotImplementedError (CUTLASS
gated-pad) or assert M%128==0 (FlashInfer). Two fixes: (a) offline
zero-pad moe_intermediate_size up to a multiple of 64*TP on the
checkpoint (pure zero-insertion on the stored pre-swizzle tensors -
numerically exact, no modelopt. The production answer, no runtime cost), or
(b) a runtime gated per-shard pad in prepare_nvfp4_moe_layer_for_fi_or_cutlass.
- The numerical gate MUST use the chat template for
-it models. A bare
/v1/completions prompt on an instruction-tuned model produces degenerate
repetition by design ("is is is...") - this is NOT a quant bug. Gate with
/v1/chat/completions. (On gemma4 the raw gate falsely reported 0/4 garbage
while the chat gate returned correct answers 4/4.) Always include a known-good
control (TP=1, or the BF16 base) when a gate fails, to attribute model-vs-gate.
Phase 3d: fused-3D experts + the model-family registry
The single biggest PTQ hazard for MoE is fused 3D expert tensors: some HF
models store experts as one nn.Parameter [num_experts, dim, dim] instead of an
nn.ModuleList of nn.Linear. A naive mtq.quantize(model, NVFP4_DEFAULT_CFG, calib_loop) walks nn.Linear modules and SILENTLY SKIPS the fused experts
(~90%+ of the model) -> a fabricated partial checkpoint (NVIDIA/Model-Optimizer
#1173). The generated quantize/quantize.py now carries a model-family
registry + a post-quant coverage guard that handles this. Use it instead of
hand-rolling:
MODEL_FAMILY registry (keyed by config.json model_type) auto-resolves
the recipe + exclude scope per family. Match the nvidia/*-NVFP4
sibling's hf_quant_config.json exclude_modules exactly (pull it from HF) so
the A/B is apples-to-apples. Worked entries: gemma4_text (native-plugin,
6-pattern experts-only excludes, needs_mm_assets), qwen3_moe (generic,
exclude lm_head + router mlp.gate), nemotron_h (generic, exclude the
Mamba/attn mixer.* projections). Add a new family with one dict entry or pass
--exclude-pattern.
- Recipes:
generic (ModuleList experts, OR fused experts a modern modelopt
auto-registers a QuantModule for) | native-plugin (this script registers
Gemma4TextExperts #1219 for the fused Gemma-4 MoE) | fail-closed (refuse a
fused MoE). generic no longer refuses a fused model upfront -- it attempts
and the coverage guard (experts quantized > 0) catches a true silent-skip,
so it works for archs modelopt natively supports without a hand-written plugin.
- transformers version determines fused-vs-ModuleList. The SAME model can load
as ModuleList on transformers 4.x and as a fused-3D module on 5.x (Qwen3-30B-A3B
is
Qwen3MoeExperts fused-3D on transformers 5.5.4). If generic hits a
coverage-0 FATAL on a fused load, either pin a transformers that exposes
ModuleList experts, or use a modelopt build that registers that arch's
QuantModule. ALWAYS read the MoE STRUCTURE DUMP the script prints first.
- Hybrid Mamba-MoE (Nemotron-H) needs
mamba-ssm + causal-conv1d in the quant
image -- its transformers modeling code hard-imports mamba_ssm at load
(vLLM serves it with its own kernels, but the transformers calibration load
does not). The stock vLLM serving image lacks them AND no prebuilt Blackwell
(sm_100) wheels exist -> build them from source CPU-side (CUDA 12.9 + nvcc
cross-compile, TORCH_CUDA_ARCH_LIST="9.0;10.0", --no-build-isolation,
*_FORCE_BUILD=TRUE, ~10 min for both), cache the wheels in object storage, and have the
quant Job pip install them. Worked: causal_conv1d 1.6.2 + mamba_ssm 2.3.2 on
CUDA 12.9 / torch 2.11 / cp312. PROVEN: Nemotron-H quantized via the generic path
(5934 experts) and reached vendor accuracy parity (gsm8k chat 0.69).
- Export
AttributeError: 'list' object has no attribute 'keys' -- some remote
modeling code (Nemotron-H) declares _tied_weights_keys as a LIST, but transformers
5.x's _get_tied_weight_keys calls .keys() (expects a dict). The generated
quantize.py normalizes list -> {k: k} for every module before
export_hf_checkpoint (generic. Fixes any list-tied-weights model on newer transformers).
Phase 3e: serve marlin-first. Calibrate with the natural-routing loop
- MARLIN first for ROBUSTNESS, but ALWAYS run the serve-backend MATRIX per-model --
do NOT conclude "marlin-only". W4A16 marlin (dequants FP4->FP16) is the robust
default that serves any NVFP4 export. But it is NOT the perf winner: on a SAME-NODE
3-way sweep, NVFP4-cutlass (W4A4 native FP4) ties FP8 and beats NVFP4-marlin by
~+21% at high throughput (Qwen3 c256), while marlin wins only the c16 memory-bound
knee and FP8 wins c1 latency -- there is NO universal winner (see
docs/METHODOLOGY.md). Cutlass degeneracy is
MODEL-SPECIFIC (Gemma's gelu_pytorch_tanh + 704-pad), NOT a property of "our
export" -- Qwen3 serves cutlass fine. So: test EVERY backend per model with a raw
"2+2" coherence gate, then run the backend x concurrency matrix sweep
(same-node, 3-trial). Never carry a backend verdict from one arch to another.
- A serve "FATAL"/"Engine core initialization failed" is a per-cell DEBUG target,
not "blocked". The real error is in the
(EngineCore pid=...) worker subprocess
(upstream of the APIServer "see root cause above" wrapper) -- read that
traceback directly. One-line root causes that have hidden behind it:
ModuleNotFoundError: pytest (marlin_fp4 -> cupy.testing lazy import, pip install pytest), AttributeError: 'list'...keys() (Nemotron-H _tied_weights_keys vs
transformers 5.x. Normalize list->dict before export), NotImplementedError ... Qwen3MoeExperts ... export (pin transformers 4.x for ModuleList experts).
- Calibrate with
create_forward_loop(cnn_dailymail) + HF_HUB_OFFLINE=0
(the quant Job must set it). The natural-routing loop on plain web text scales
the experts correctly. A chat-control-token-wrapped calib corpus SKEWS the
per-expert amax -> degenerate output even with a correct plugin + serve path.
Keep a plain-text JSONL only as the offline fallback.
- Pad the MoE intermediate ONLY if a backend rejects the alignment. Most
models are already 64-aligned (Qwen3 768). Gemma-4's 704 needed the offline
704->768 pad.
PAD_TARGET=0 (skip) is the common case -- do not pad reflexively.
-it accuracy gate = 0-shot raw + chat (e.g. a gsm8k probe), n>=400 for a
tight A/B. Few-shot raw transcripts make an -it model emit end-of-turn (empty).
Teardown
Tear down the quantize Job + calib Job by label
(kubectl delete job -l experiment=<id-slug>). Keep the output PVC (it holds the
champion weights) until the parent run promotes or discards them, then delete it
PV-last per the experiment-isolation rule.
Calibration notes
- Sample count: NVFP4 PTQ converges with a few hundred calibration samples,
more rarely helps and costs GPU time. Start at 256-512.
- Domain match: use a calibration corpus representative of the serving
workload when accuracy is marginal -- the calib set shapes the per-layer scales.
- KV scales: NVFP4 KV is blocked for MLA models in the current serving vLLM
(see the GLM/Kimi notes). This skill quantizes weights + activation scales.
Set
--kv-cache-dtype at serve time as a separate lever.
Verdict rigor (DRAFT vs VERDICT)
Accuracy deltas are node-independent (a quant either changes the logits or it
does not), so a single eval run is a valid accuracy VERDICT. The perf half of
the gate follows the standard rule -- DRAFT unless same-node + >=3 trials +
metric-isolated. Report the accuracy and perf verdicts separately. Never let a
perf win paper over an accuracy regression.
Safety
- Ack-gated -- the quantize Job consumes a GPU node. It fails closed without
the explicit intent flag (
safety=submits_jobs), per
server/docs/mcp-tool-io-contract.md.
- Experiment isolation -- the output PVC + both Jobs are experiment-prefixed
and carry
experiment=<id-slug>. Never reuse a standing *-cache PVC. Tear
down by label, PV last.
- Scheduling (cluster-profile aware) -- plain-K8s GB300:
default-scheduler +
kubectl GPU preflight + emptyDir/hf-pull + object-storage output. Legacy Slurm-on-K8s: the
quantize Job uses the Slurm scheduler + the lock toleration + sinfo/squeue.
- No accuracy bypass -- the accuracy gate is mandatory. A quant is not "done"
until it has been eval'd against the base. No silent fallback to "ship it
anyway".
Source-of-truth references
- The vendor-checkpoint assertion (
quant_method in {modelopt, modelopt_fp4, NVFP4}) this skill's output must satisfy.
inference-model-eval -- the accuracy gate.
- NVIDIA TensorRT Model Optimizer (
modelopt) PTQ + llm-compressor oneshot
upstream docs (pinned in quantize/README.md of the generated bundle).
server/AGENTS.md "All performance numbers..." + "Verdict rigor" +
"Experiment Isolation & Traceability".
Full-context reporting (no bare numbers)
Per the methodology canon "Every performance number carries its full context (no bare numbers)"
(docs/METHODOLOGY.md): every number this
skill emits MUST carry its full measurement-context descriptor, and every comparison MUST be
matched on it. A bare tok/s / TPOT / BW / %SoL / speedup is a defect - it cannot set a
default, ship a config, or appear in a report.
- Identity: model (+HF path), hardware (exact ceiling token
GB300/B200), quant, kv-cache dtype.
- Parallelism: TP, DP (replicas), PP, EP, parallel_strategy.
- Serving cfg: max-num-seqs, max-num-batched-tokens, gpu-memory-utilization, max-model-len, cudagraph_mode/enforce_eager, async_scheduling, prefix-caching.
- Workload: dataset, ISL/OSL (or mean in/out tokens), concurrency, num-prompts.
- Regime: warm vs cold. Latency vs throughput tier.
- Stack: image/vllm commit, bench backend, serving engine.
- Grounding:
%SoL (+ ceiling key from configs/sol-ceilings.yaml - never inline a peak), sol_rigor (L1-L4), trials n (mean±std), same-node, baseline named.
- Per-number exact shape (no smoothing): when reporting more than one number, keep EACH with its own exact shape (ISL/OSL, concurrency, dataset, regime) - never normalize a set to one uniform descriptor that hides per-point variation (e.g.
c=1 @ ISL1024/OSL256 + c=64 @ ISL4096/OSL512, NOT one shared "random").
If this skill produces a measurement (tok/s, latency, %SoL, speedup), follow the
rigor discipline: capture L1 zymtrace + L3 DCGM (L4 ncu where feasible)
Speed-of-Light and publish --strict. Canonical map: server/AGENTS.md
"Rigor discipline index" / docs/METHODOLOGY.md. Skills that
do not produce measurements are exempt (server/AGENTS.md "Speed-of-light framing").
Next lever / BREAKTHROUGH (Grind Mandate)
If this skill emits a measured result, its output MUST end by naming the next perf lever,
its expected unlock (direction + rough magnitude), and the gate that proves/refutes it,
per "The Grind Mandate" (server/AGENTS.md + docs/METHODOLOGY.md). A
measured win is the new floor, not the finish -- so do everything we can to find the next
BREAKTHROUGH: the highest-EV unlock toward Speed-of-Light (a new champion / kernel / router /
quant / parallelism / spec-decode win, or an unblocked stack), not just the next micro-lever.
Rank the candidate breakthrough levers by value x cost (the GRIND FRONTIER, perftunereport value_view), pursue the top, bank the rest with evidence. Record WHY a refuted lever loses,
update the standing frontier in the active bundle's HANDOFF.md. Never conclude
"exhausted/optimal/done" without an explicit next-lever frontier (an empty frontier AND a
documented SoL wall only). Delete this section ONLY if the skill produces no measurements.
1---2name: inference-quantize-calibrate3description: Produce quantized inference weights from a BF16/FP8 base checkpoint via a post-training-quantization (PTQ) pipeline -- instead of only ever pulling NVFP4 weights pre-quantized. A calibration prep Job then a quantize Job: NVIDIA ModelOpt -> NVFP4 (default), or llm-compressor -> FP8 (alt). Writes weights + per-layer scales to an experiment-prefixed PVC, gated on inference-model-eval accuracy + a perf A/B. When a vendor nvidia/*-NVFP4 exists, runs a 3-way comparison (base vs ours vs NVIDIA) on accuracy + perf + ncu. Ack-gated. The calibrate+quantize phase of inference-model-optimize. Triggers on "quantize a model", "calibrate NVFP4", "run modelopt PTQ", "produce NVFP4 weights", "llm-compressor FP8", "PTQ pipeline", "compare our PTQ to NVIDIA NVFP4", "our nvfp4 vs nvidia nvfp4", "calibration dataset for quantization", "quantize <model> to NVFP4/FP8", or any combination of "quantize / calibrate / PTQ / modelopt / llm-compressor" with "nvfp4 / fp8 / weights / model / inference".4license: MIT5---67# inference-quantize-calibrate89> **Fast model loading (always-applied):** when staging a base checkpoint or standing up a vLLM deploy, never load 100s-of-GB single-stream via s3fs FUSE (a GLM-5.1-sized model takes ~50 min that way. See `docs/METHODOLOGY.md`). Prefer, in order: a fast model-loading endpoint when reachable -> parallel multipart to local NVMe (`server/tools/stage-model-parallel.py`) -> `runai_model_streamer` (`--load-format runai_streamer`) -> tensorizer. Flag a slow load loudly: if effective rate < ~500 MB/s on a large model, STOP and switch. Details: `server/docs/inference-fast-model-loading.md`.1011## Purpose1213Produce a quantized checkpoint **from a base model** -- rather than relying on14NVFP4 weights *pulled pre-quantized* from `nvidia/*-NVFP4` HF repos. This skill15provides the calibrate + quantize recipe for a fresh model as a16two-Job PTQ pipeline:17181. **Calibration-dataset prep Job** -- tokenizes a small calibration corpus19 (default: a few hundred samples of C4 / the model's chat template) into the20 format the quantizer's PTQ calibration loop expects.212. **Quantize Job** -- runs the quantizer over the base weights with the22 calibration set, emitting quantized weights + per-layer activation/KV scales.2324Two pluggable backends:2526| Backend | Target | Use when |27| --- | --- | --- |28| **NVIDIA ModelOpt** (`modelopt`, default) | **NVFP4** | Matches the `nvidia/*-NVFP4` serving checkpoints (modelopt 0.45, `quant_algo=NVFP4`, group_size 16). |29| **llm-compressor** | **FP8** (compressed-tensors) | FP8 weight quant for an FP8-tier deploy or an FP8-vs-NVFP4 A/B. |3031The output PVC is experiment-prefixed and the result feeds straight into a deploy32+ accuracy/perf gate.3334## When to use3536- A new model has no published NVFP4/FP8 variant and you need quantized weights37 to serve it on Blackwell efficiently.38- You want to reproduce / improve on a vendor NVFP4 checkpoint with your own39 calibration set (e.g. a domain-specific calib corpus).40- The `inference-model-optimize` orchestrator reached Phase 4.4142Do **not** use this skill for:4344- Models that already have a good vendor NVFP4/FP8 checkpoint you trust -- just45 pull the vendor checkpoint (cheaper, byte-for-byte vendor recipe).46- KV-cache dtype selection at serve time (`--kv-cache-dtype=fp8_e4m3`) -- that is47 a `vllm.extraArgs` lever, not weight PTQ. Tune it with48 [`inference-tune-sweep`](../inference-tune-sweep/SKILL.md).49- Runtime activation-quant kernel work --50 that is custom-kernel territory, not this PTQ pipeline.5152## Example prompts5354- "Quantize MiniMax-M2.7 to NVFP4 with modelopt and validate accuracy."55- "Run an FP8 llm-compressor PTQ on this model and A/B it against BF16."56- "Produce NVFP4 weights for `zai-org/GLM-5.2` with a C4 calibration set."57- `/inference-quantize-calibrate --base /models/base --backend modelopt --target nvfp4`5859## Prerequisites6061The skill **fails closed** if any of these are not satisfied.62631. The **base** (BF16 or FP8) checkpoint reachable (HF id / S3) -- you quantize64 *from* the base, not the NVFP4 serving copy. On the current GB300 (4 GPU/node)65 a large MoE base may need TP across 2 NVLink-domain nodes. B200 fits 8/node.662. Output persistence (cluster-profile aware): on plain-K8s (no storageclass)67 write to emptyDir + upload to S3-compatible object storage from inside the68 pod. On legacy Slurm-on-K8s, an experiment-prefixed PVC69 (never reuse a standing `*-cache` PVC).703. Cluster access + a free GPU node: plain-K8s GB300 (default) via71 `kubectl` GPU requests. Legacy Slurm-on-K8s via `sinfo`/`squeue`. The quantize Job is72 GPU-resident for calibration forward passes.734. The quantizer container image (modelopt or llm-compressor) -- the `quantize/`74 templates pin one. Air-gapped clusters mirror it first.755. The explicit ack flag for the quantize Job (it consumes a GPU node).7677## Workflow7879### Phase 0: confirm intent + scaffold the quant bundle8081State back: `base` path, `backend` (`modelopt` | `llm-compressor`), `target`82(`nvfp4` | `fp8`), calibration corpus + sample count, output PVC name83(experiment-prefixed), and the experiment-id (== the parent run-id). Read the84base `config.json` for arch + chat template so calibration uses the right85formatting. The scaffolder lays down `quantize/`:8687- `quantize/00-calib-pvc.yaml` -- experiment-prefixed output PVC.88- `quantize/01-calib-dataset-job.yaml` -- calibration-dataset prep Job.89- `quantize/02-quantize-job.yaml` -- the PTQ quantize Job (backend-parameterized).90- `quantize/quantize.py` -- the backend dispatcher (modelopt / llm-compressor).91- `quantize/README.md` -- the per-bundle runbook.9293### Phase 1: prepare the calibration set9495Apply `01-calib-dataset-job.yaml`. It tokenizes the calibration corpus with the96model's tokenizer + chat template into the quantizer's expected JSONL/arrow and97writes it to the output PVC. Gate: Job Complete + the calib file exists and has98the requested sample count.99100### Phase 2: run the quantize Job (ack-gated)101102```text103# render the manifest and verify that both lines equal <id-slug>:104kubectl -n <slurm-namespace> apply --dry-run=server \105 -f quantize/02-quantize-job.yaml \106 -o jsonpath='{.metadata.labels.experiment}{"\n"}{.spec.template.metadata.labels.experiment}{"\n"}'107# then apply (the Job carries the ack/intent in an env flag the entrypoint checks):108kubectl -n <slurm-namespace> apply -f quantize/02-quantize-job.yaml109```110111The Job runs `quantize.py`:112113- **modelopt path**: `mtq.quantize(model, NVFP4_DEFAULT_CFG, calib_loop)` then114 export to a HF-layout NVFP4 checkpoint (`quant_algo=NVFP4`, group_size 16) +115 `hf_quant_config.json`, so vLLM auto-detects it exactly like a116 `nvidia/*-NVFP4` repo.117- **llm-compressor path**: a `oneshot` FP8 recipe (compressed-tensors), emitting118 per-layer scales.119120Outputs land at `/models/quantized/target` on the experiment PVC. Gate: Job121Complete + the output `config.json`'s quant method is the expected one122(`modelopt`/`modelopt_fp4`/`NVFP4` for NVFP4, `compressed-tensors`/`fp8` for FP8)123-- the same assertion the `03b` puller makes on a vendor checkpoint.124125### Phase 3: validate the quantized weights (the gate that matters)126127Deploy the quantized weights (the parent orchestrator's Phase 5, or a standalone128deploy off the experiment PVC) and run:129130- [`inference-model-eval`](../inference-model-eval/SKILL.md) (GPQA + MMLU-Pro)131 vs the **base** model's scores.132- A perf A/B (throughput + median TPOT) vs the base via133 [`inference-tune-sweep`](../inference-tune-sweep/SKILL.md) /134 [`inference-perf-bench`](../inference-perf-bench/SKILL.md).135136Gate: accuracy delta within tolerance (default: no metric drops > 1.0 absolute137point) **AND** a perf win. A quant that wins perf but fails accuracy is a138**no-ship** -- report the regression and keep the base/vendor checkpoint. Record139the accuracy + perf result as a baseline so future re-quants diff against it.140141### Phase 3b: 3-way comparison vs the vendor NVFP4 (our-PTQ vs NVIDIA-PTQ)142143When a vendor `nvidia/*-NVFP4` (or `*-FP8`) checkpoint exists for the same model,144"is our PTQ good enough" is the wrong question -- the decision is **which of the145two NVFP4 checkpoints to ship**. Run a head-to-head **3-way comparison** across146three variants on one pinned node:147148| Variant | Source | Role |149| --- | --- | --- |150| `base` | BF16/FP8 base | accuracy ceiling reference |151| `ours` | our ModelOpt NVFP4 (Phase 2 output) | candidate |152| `nvidia` | `nvidia/*-NVFP4` (pulled from HF) | incumbent baseline |153154Compare on three axes, each artifact-cited:1551561. **Accuracy** -- [`inference-model-eval`](../inference-model-eval/SKILL.md)157 (GPQA + MMLU-Pro) for all three. Report `ours` and `nvidia` as deltas vs158 `base`.1592. **Perf** -- median TPOT + throughput, same-node + >=3 trials160 ([`inference-tune-sweep`](../inference-tune-sweep/SKILL.md) controlled A/B).1613. **Per-kernel** -- [`inference-kernel-ncu-profile`](../inference-kernel-ncu-profile/SKILL.md)162 on the dominant decode kernel of each, to explain *why* one wins (tensor-core163 engagement, achieved roofline %) rather than asserting it.164165Implement as a `perftunereport` campaign with **one cell per variant** (`base` /166`ours` / `nvidia`) so the three land side-by-side in the atlas + PDF + perf-lake167automatically, and the verdict ("ship ours" / "ship vendor" / "tie -> keep168vendor") is a published, data-backed row -- not a claim. A common honest outcome169is parity, in which case the vendor checkpoint wins on provenance. Record that.170171### Phase 3c: NVFP4-MoE serving gotchas172173A quantized MoE checkpoint can fail to *serve* (or serve garbage) for reasons174unrelated to PTQ quality. Three gotchas, each with the fix:1751761. **Activation-capability gap.** The native FP4 MoE kernels (CUTLASS,177 FlashInfer trtllm-gen) only advertise a fixed activation set. A model whose178 expert FFN uses an unlisted activation (gemma4 = `gelu_pytorch_tanh` ->179 `MoEActivation.GELU_TANH`) is rejected at engine init even though the weights180 are fine. Fix: add the activation to the kernel class `_supports_activation`181 (CUTLASS routes non-SiLU through the generic `apply_moe_activation`, so it is182 often a Python-only add) + the FlashInfer `ACTIVATION_TO_FI_ACTIVATION` map183 (note: FlashInfer has no tanh-gelu variant -> trtllm maps GELU_TANH -> Geglu,184 an *approximation*. The exact path is CUTLASS).1852. **Intermediate alignment is TP-dependent.** The FP4 MoE kernels need the186 per-rank expert intermediate `%64` (and the fused gate+up `2N %128` for187 trtllm). `moe_intermediate_size` that is fine at TP=1 can break at TP=4188 (gemma4: 704 -> per-rank 176, not %64) with a `NotImplementedError` (CUTLASS189 gated-pad) or `assert M%128==0` (FlashInfer). Two fixes: (a) **offline**190 zero-pad `moe_intermediate_size` up to a multiple of `64*TP` on the191 checkpoint (pure zero-insertion on the stored pre-swizzle tensors -192 numerically exact, no modelopt. The production answer, no runtime cost), or193 (b) a **runtime** gated per-shard pad in `prepare_nvfp4_moe_layer_for_fi_or_cutlass`.1943. **The numerical gate MUST use the chat template for `-it` models.** A bare195 `/v1/completions` prompt on an instruction-tuned model produces degenerate196 repetition by design ("is is is...") - this is NOT a quant bug. Gate with197 `/v1/chat/completions`. (On gemma4 the raw gate falsely reported 0/4 garbage198 while the chat gate returned correct answers 4/4.) Always include a known-good199 control (TP=1, or the BF16 base) when a gate fails, to attribute model-vs-gate.200201### Phase 3d: fused-3D experts + the model-family registry202203The single biggest PTQ hazard for MoE is **fused 3D expert tensors**: some HF204models store experts as one `nn.Parameter [num_experts, dim, dim]` instead of an205`nn.ModuleList` of `nn.Linear`. A naive `mtq.quantize(model, NVFP4_DEFAULT_CFG,206calib_loop)` walks `nn.Linear` modules and **SILENTLY SKIPS** the fused experts207(~90%+ of the model) -> a fabricated partial checkpoint (NVIDIA/Model-Optimizer208#1173). The generated `quantize/quantize.py` now carries a **model-family209registry** + a post-quant **coverage guard** that handles this. Use it instead of210hand-rolling:211212- **`MODEL_FAMILY` registry** (keyed by `config.json` `model_type`) auto-resolves213 the **recipe** + **exclude scope** per family. Match the `nvidia/*-NVFP4`214 sibling's `hf_quant_config.json` `exclude_modules` exactly (pull it from HF) so215 the A/B is apples-to-apples. Worked entries: `gemma4_text` (native-plugin,216 6-pattern experts-only excludes, `needs_mm_assets`), `qwen3_moe` (generic,217 exclude `lm_head` + router `mlp.gate`), `nemotron_h` (generic, exclude the218 Mamba/attn `mixer.*` projections). Add a new family with one dict entry or pass219 `--exclude-pattern`.220- **Recipes**: `generic` (ModuleList experts, OR fused experts a modern modelopt221 auto-registers a QuantModule for) | `native-plugin` (this script registers222 Gemma4TextExperts #1219 for the fused Gemma-4 MoE) | `fail-closed` (refuse a223 fused MoE). `generic` no longer *refuses* a fused model upfront -- it attempts224 and the **coverage guard** (experts quantized > 0) catches a true silent-skip,225 so it works for archs modelopt natively supports without a hand-written plugin.226- **transformers version determines fused-vs-ModuleList.** The SAME model can load227 as ModuleList on transformers 4.x and as a fused-3D module on 5.x (Qwen3-30B-A3B228 is `Qwen3MoeExperts` fused-3D on transformers 5.5.4). If `generic` hits a229 coverage-0 FATAL on a fused load, either pin a transformers that exposes230 ModuleList experts, or use a modelopt build that registers that arch's231 QuantModule. ALWAYS read the MoE STRUCTURE DUMP the script prints first.232- **Hybrid Mamba-MoE (Nemotron-H) needs `mamba-ssm` + `causal-conv1d` in the quant233 image** -- its transformers modeling code hard-imports `mamba_ssm` at load234 (vLLM serves it with its own kernels, but the transformers calibration load235 does not). The stock vLLM serving image lacks them AND no prebuilt Blackwell236 (sm_100) wheels exist -> **build them from source CPU-side** (CUDA 12.9 + nvcc237 cross-compile, `TORCH_CUDA_ARCH_LIST="9.0;10.0"`, `--no-build-isolation`,238 `*_FORCE_BUILD=TRUE`, ~10 min for both), cache the wheels in object storage, and have the239 quant Job `pip install` them. Worked: causal_conv1d 1.6.2 + mamba_ssm 2.3.2 on240 CUDA 12.9 / torch 2.11 / cp312. PROVEN: Nemotron-H quantized via the generic path241 (5934 experts) and reached vendor accuracy parity (gsm8k chat 0.69).242- **Export `AttributeError: 'list' object has no attribute 'keys'`** -- some remote243 modeling code (Nemotron-H) declares `_tied_weights_keys` as a LIST, but transformers244 5.x's `_get_tied_weight_keys` calls `.keys()` (expects a dict). The generated245 `quantize.py` normalizes list -> `{k: k}` for every module before246 `export_hf_checkpoint` (generic. Fixes any list-tied-weights model on newer transformers).247248### Phase 3e: serve marlin-first. Calibrate with the natural-routing loop249250- **MARLIN first for ROBUSTNESS, but ALWAYS run the serve-backend MATRIX per-model --251 do NOT conclude "marlin-only".** W4A16 marlin (dequants FP4->FP16) is the robust252 default that serves any NVFP4 export. But it is NOT the perf winner: on a SAME-NODE253 3-way sweep, NVFP4-**cutlass** (W4A4 native FP4) ties FP8 and beats NVFP4-marlin by254 ~+21% at high throughput (Qwen3 c256), while marlin wins only the c16 memory-bound255 knee and FP8 wins c1 latency -- there is NO universal winner (see256 `docs/METHODOLOGY.md`). Cutlass degeneracy is257 **MODEL-SPECIFIC** (Gemma's `gelu_pytorch_tanh` + 704-pad), NOT a property of "our258 export" -- Qwen3 serves cutlass fine. So: test EVERY backend per model with a raw259 "2+2" coherence gate, then run the backend x concurrency matrix sweep260 (same-node, 3-trial). Never carry a backend verdict from one arch to another.261- **A serve "FATAL"/"Engine core initialization failed" is a per-cell DEBUG target,262 not "blocked".** The real error is in the `(EngineCore pid=...)` worker subprocess263 (upstream of the APIServer "see root cause above" wrapper) -- read that264 traceback directly. One-line root causes that have hidden behind it:265 `ModuleNotFoundError: pytest` (marlin_fp4 -> cupy.testing lazy import, `pip install266 pytest`), `AttributeError: 'list'...keys()` (Nemotron-H `_tied_weights_keys` vs267 transformers 5.x. Normalize list->dict before export), `NotImplementedError ...268 Qwen3MoeExperts ... export` (pin transformers 4.x for ModuleList experts).269- **Calibrate with `create_forward_loop(cnn_dailymail)` + `HF_HUB_OFFLINE=0`**270 (the quant Job must set it). The natural-routing loop on plain web text scales271 the experts correctly. A chat-control-token-wrapped calib corpus SKEWS the272 per-expert amax -> degenerate output even with a correct plugin + serve path.273 Keep a plain-text JSONL only as the offline fallback.274- **Pad the MoE intermediate ONLY if a backend rejects the alignment.** Most275 models are already 64-aligned (Qwen3 768). Gemma-4's 704 needed the offline276 704->768 pad. `PAD_TARGET=0` (skip) is the common case -- do not pad reflexively.277- **`-it` accuracy gate = 0-shot raw + chat** (e.g. a gsm8k probe), n>=400 for a278 tight A/B. Few-shot raw transcripts make an -it model emit end-of-turn (empty).279280### Teardown281282Tear down the quantize Job + calib Job by label283(`kubectl delete job -l experiment=<id-slug>`). Keep the output PVC (it holds the284champion weights) until the parent run promotes or discards them, then delete it285PV-last per the experiment-isolation rule.286287## Calibration notes288289- **Sample count**: NVFP4 PTQ converges with a few hundred calibration samples,290 more rarely helps and costs GPU time. Start at 256-512.291- **Domain match**: use a calibration corpus representative of the serving292 workload when accuracy is marginal -- the calib set shapes the per-layer scales.293- **KV scales**: NVFP4 KV is blocked for MLA models in the current serving vLLM294 (see the GLM/Kimi notes). This skill quantizes *weights* + activation scales.295 Set `--kv-cache-dtype` at serve time as a separate lever.296297## Verdict rigor (DRAFT vs VERDICT)298299Accuracy deltas are node-independent (a quant either changes the logits or it300does not), so a single eval run is a valid accuracy VERDICT. The **perf** half of301the gate follows the standard rule -- DRAFT unless same-node + >=3 trials +302metric-isolated. Report the accuracy and perf verdicts separately. Never let a303perf win paper over an accuracy regression.304305## Safety306307- **Ack-gated** -- the quantize Job consumes a GPU node. It fails closed without308 the explicit intent flag (`safety=submits_jobs`), per309 [`server/docs/mcp-tool-io-contract.md`](../../server/docs/mcp-tool-io-contract.md).310- **Experiment isolation** -- the output PVC + both Jobs are experiment-prefixed311 and carry `experiment=<id-slug>`. Never reuse a standing `*-cache` PVC. Tear312 down by label, PV last.313- **Scheduling (cluster-profile aware)** -- plain-K8s GB300: `default-scheduler` +314 `kubectl` GPU preflight + emptyDir/hf-pull + object-storage output. Legacy Slurm-on-K8s: the315 quantize Job uses the Slurm scheduler + the lock toleration + `sinfo`/`squeue`.316- **No accuracy bypass** -- the accuracy gate is mandatory. A quant is not "done"317 until it has been eval'd against the base. No silent fallback to "ship it318 anyway".319320## Source-of-truth references321322- The vendor-checkpoint assertion (`quant_method in {modelopt, modelopt_fp4,323 NVFP4}`) this skill's output must satisfy.324- [`inference-model-eval`](../inference-model-eval/SKILL.md) -- the accuracy gate.325- NVIDIA TensorRT Model Optimizer (`modelopt`) PTQ + llm-compressor `oneshot`326 upstream docs (pinned in `quantize/README.md` of the generated bundle).327- `server/AGENTS.md` "All performance numbers..." + "Verdict rigor" +328 "Experiment Isolation & Traceability".329330## Full-context reporting (no bare numbers)331332Per the methodology canon "Every performance number carries its full context (no bare numbers)"333(`docs/METHODOLOGY.md`): every number this334skill emits MUST carry its full measurement-context descriptor, and every comparison MUST be335matched on it. A bare `tok/s` / TPOT / BW / %SoL / speedup is a defect - it cannot set a336default, ship a config, or appear in a report.337- **Identity:** model (+HF path), hardware (exact ceiling token `GB300`/`B200`), quant, kv-cache dtype.338- **Parallelism:** TP, DP (replicas), PP, EP, parallel_strategy.339- **Serving cfg:** max-num-seqs, max-num-batched-tokens, gpu-memory-utilization, max-model-len, cudagraph_mode/enforce_eager, async_scheduling, prefix-caching.340- **Workload:** dataset, ISL/OSL (or mean in/out tokens), concurrency, num-prompts.341- **Regime:** warm vs cold. Latency vs throughput tier.342- **Stack:** image/vllm commit, bench backend, serving engine.343- **Grounding:** `%SoL` (+ ceiling key from `configs/sol-ceilings.yaml` - never inline a peak), sol_rigor (L1-L4), trials n (mean±std), same-node, baseline named.344- **Per-number exact shape (no smoothing):** when reporting more than one number, keep EACH with its own exact shape (ISL/OSL, concurrency, dataset, regime) - never normalize a set to one uniform descriptor that hides per-point variation (e.g. `c=1 @ ISL1024/OSL256` + `c=64 @ ISL4096/OSL512`, NOT one shared "random").345346If this skill produces a measurement (tok/s, latency, %SoL, speedup), follow the347rigor discipline: capture L1 zymtrace + L3 DCGM (L4 ncu where feasible)348Speed-of-Light and publish `--strict`. Canonical map: `server/AGENTS.md`349"Rigor discipline index" / `docs/METHODOLOGY.md`. Skills that350do not produce measurements are exempt (`server/AGENTS.md` "Speed-of-light framing").351352## Next lever / BREAKTHROUGH (Grind Mandate)353354If this skill emits a measured result, its output MUST end by naming the **next perf lever**,355its **expected unlock** (direction + rough magnitude), and the **gate** that proves/refutes it,356per "The Grind Mandate" (`server/AGENTS.md` + `docs/METHODOLOGY.md`). A357measured win is the new floor, not the finish -- so **do everything we can to find the next358BREAKTHROUGH**: the highest-EV unlock toward Speed-of-Light (a new champion / kernel / router /359quant / parallelism / spec-decode win, or an unblocked stack), not just the next micro-lever.360Rank the candidate breakthrough levers by value x cost (the GRIND FRONTIER, `perftunereport361value_view`), pursue the top, bank the rest with evidence. Record WHY a refuted lever loses,362update the standing frontier in the active bundle's `HANDOFF.md`. Never conclude363"exhausted/optimal/done" without an explicit next-lever frontier (an empty frontier AND a364documented SoL wall only). Delete this section ONLY if the skill produces no measurements.