Skip to content

[megatron] 1/n towards Kimi K2.6: fake QAT training for Qwen3.6-35B-INT4#1862

Open
casper-hansen wants to merge 5 commits into
NovaSky-AI:mainfrom
casper-hansen:casper/fake-int4-qat
Open

[megatron] 1/n towards Kimi K2.6: fake QAT training for Qwen3.6-35B-INT4#1862
casper-hansen wants to merge 5 commits into
NovaSky-AI:mainfrom
casper-hansen:casper/fake-int4-qat

Conversation

@casper-hansen

Copy link
Copy Markdown
Contributor

INT4 serving now adds essentially nothing over BF16 serving. The gap with fake-quant on equals the same-weights floor. The example for Megatron uses https://huggingface.co/casperhansen/Qwen3.6-35B-A3B-INT4-RTN which includes the code to use vLLM project's llm-compressor to produce these weights.

The difference between the Qwen3.6 INT4 weights and Kimi is a slight nuance in the SCALE_DIV and Q_MIN variable which is intended to be forward compatible when Kimi K2.6 is supported at a later time.

Results

STE removes ~55% of the uncorrected train/infer gap, halves the spread, and cuts the tail hardest.

arm mean (per step) avg mean avg std worst max
OFF (no fake-quant) 0.0259 – 0.0315 0.0279 0.086 4.99
STE (bit-exact) 0.0118 – 0.0141 0.0126 0.040 2.24

Can we get closer to zero?

The remaining 0.0118 is the cross-engine numerics floor, and we're sitting exactly on it. The cleanest evidence is STE step 1: at that point the LoRA adapters are still zero and the expert grid is bit-exact, so trainer and vLLM score bit-identical weights and the gap is 0.0118.

Inspiration

This was inspired by the work done in miles https://github.com/radixark/miles/ on the Megatron side.

Fake-quantize the frozen fused MoE expert GEMMs (TEGroupedLinear) onto the
served compressed-tensors INT4 grid inside the forward pass, with a
straight-through-estimator backward so gradients still reach the BF16 masters
(LoRA adapters stay BF16, matching "INT4 base + BF16 adapter" at inference).
This removes the train/infer log-prob gap when vLLM serves the experts as INT4
(e.g. Qwen3.6-35B-A3B-INT4-RTN) while the Megatron trainer holds BF16 masters.

- fake_int4_qat.py: group-symmetric INT4 STE; scale_divisor=7.5 matches
  llm-compressor/compressed-tensors RTN (bit-exact), 7.0 matches Kimi/Miles.
- config: trainer.policy.model.fake_int4_qat (enabled, group_size,
  scale_divisor, bf16_base_path).
- megatron_worker: install the STE hook and, since Megatron-Bridge cannot load
  a compressed-tensors INT4 checkpoint, redirect the trainer's BF16 master
  weights via bf16_base_path while keeping the INT4 model.path as the logical
  identity (so saved LoRA adapters reference the INT4 base). Ref worker mirrors
  the policy. The BF16 redirect is independent of the STE so an INT4-served
  baseline (fake-quant off) still loads.
- example: new run_megatron_dapo_qwen3.6_35b_a3b_lora_int4_qat.sh -- a 1-node
  H200 INT4-QAT ablation (QAT_MODE off/on) with the fused LM head, leaving the
  original run_megatron_dapo_qwen3.6_35b_a3b_lora.sh untouched.
…ors grid

The STE previously computed the quantization grid in fp32 with an eps floor on
the scale, which diverged from the artifact the inference engine actually
serves: llm-compressor/compressed-tensors produces the grid in the weight dtype
(scale = rn_bf16(amax/divisor), divide+round in bf16, bf16-multiply dequant),
and near-denormal groups (which real checkpoints contain) were zeroed by the
eps clamp. Measured against Qwen3.6-35B-A3B-INT4-RTN, only ~89% of expert
weight elements landed on the served grid (residual RMS 4.3e-4 of the 1.03e-3
no-QAT gap).

Rework the STE forward to mirror the compressed-tensors quantize->dequantize
arithmetic exactly: fp32 amax with a single rounding into the weight dtype for
the scale, grid math in the weight dtype, and a zero-guard instead of the eps
clamp. Verified bit-exact (100.000000% of elements, RMS 0) against both
supported artifact conventions:

- Qwen3.6-35B-A3B-INT4-RTN (scale_divisor=7.5, q_min=-8), 75.5M elements
- moonshotai/Kimi-K2.6 (scale_divisor=7.0, q_min=-7), 50.7B elements across
  layers 1/30/60 with BF16 masters dequantized from the INT4 release

Adds fake_int4_qat.q_min to the config for the Kimi convention, whose QAT
pins codes to [-7, 7] and never emits -8.

A/B on the INT4-QAT example (QAT_MODE=on, 5 steps, 8xH200):
policy/rollout_train_logprobs_abs_diff_mean drops from 0.0156-0.0189 (old STE)
to 0.0118-0.0141 (fixed), std 0.053 -> 0.040, max spikes 4.4 -> 2.2, TIS
is_ratio_max 17.4 -> 3.6, tis_clip_frac 1e-4 -> 0.
The (scale_divisor, q_min) convention pair is fully specified by
trainer.policy.model.fake_int4_qat, so the module-level SCALE_DIV_* /
Q_MIN_* constants were redundant defaults that could silently drift from
the config's. Remove them and make the STE / installer parameters
required, so every caller states the convention explicitly (the worker
already passes all values straight from the config).

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces fake-INT4 quantization-aware training (QAT) for Megatron MoE experts, addressing the train/infer log-prob mismatch when serving with INT4 but training with BF16. It adds a straight-through estimator (STE) for group-symmetric INT4 fake-quantization, integrates it into the Megatron worker, and updates the configuration schema and training scripts. The review feedback suggests replacing ".view()" with ".reshape()" in the fake-quantization forward pass to robustly handle potentially non-contiguous tensors and prevent runtime errors.

Comment thread skyrl/backends/skyrl_train/workers/megatron/fake_int4_qat.py Outdated
Comment thread skyrl/backends/skyrl_train/workers/megatron/fake_int4_qat.py Outdated
Black (pre-commit, 24.10.0) wants the conditional expression in the
_get_weight_tensors patch parenthesized; the code-quality job runs it
over all files so the branch was failing CI.

Also switch the two grid .view() calls to .reshape() (Gemini review):
the TE expert weights the hook sees are always contiguous (reshape is
then exactly view, no copy), but fake_int4_quantize_ste is a public
helper and a sliced/transposed 2D input would make .view raise; reshape
handles it by copying. Verified bit-exactness is unchanged and
non-contiguous inputs now round-trip correctly.
CPU tier (runs in the cheap CI job, no megatron/TE needed):
- golden bit-exactness tests pinning the STE to slices of the real served
  artifacts (Qwen3.6 INT4-RTN with true BF16 masters incl. near-denormal
  groups and stored -8 codes; Kimi K2.6 with dequant masters), stored as
  bit patterns so any grid-arithmetic drift fails exactly
- STE properties: identity backward, main_grad propagation, padding,
  all-zero groups, convention code ranges, div-7 idempotence/identity,
  non-contiguous inputs
- config defaults + CLI overrides for fake_int4_qat

GPU tier (gpu_ci/megatron, guards megatron-core/TE bumps):
- tripwire that TEGroupedLinear._get_weight_tensors exists and is still
  consumed by TE GroupedLinear.forward, with actionable failure messages
- hook-effect canary: patched forward must equal the same TE grouped GEMM
  run on pre-quantized weights bitwise, differ from the unpatched forward,
  and track in-place master mutations (catches silent no-op and future
  weight caching)
- _maybe_setup_fake_int4_qat wiring (redirect independent of STE install)
- worker-level forward on truncated Qwen3-30B-A3B (2 layers, TP4/EP4):
  QAT on/off must change logprobs moderately through the real init path
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant