Skip to content

Kimi-K3 on layerwise fused export: 3 defect fixes, multimodal support, single-B200 NVFP4 - #2218

Draft
Fridah-nv wants to merge 6 commits into
fridah/layerwise-fused-exportfrom
fridah/k3-layerwise-fused-export
Draft

Kimi-K3 on layerwise fused export: 3 defect fixes, multimodal support, single-B200 NVFP4#2218
Fridah-nv wants to merge 6 commits into
fridah/layerwise-fused-exportfrom
fridah/k3-layerwise-fused-export

Conversation

@Fridah-nv

@Fridah-nv Fridah-nv commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Type of change: Bug fix + new feature

Stacked on #2136. Brings per-layer fused export up on a model that genuinely needs it —
moonshotai/Kimi-K3: 1.5 TB, 896 experts, 93 layers, a VLM — quantized to NVFP4 on a
single B200.

Rebased onto #2136 (2026-08-21). #2136's branch was rewritten, so six of this PR's
original commits were stale copies of work that has since landed there — the resume-manifest
and orphan-shard fixes among them. Those are dropped, not lost: they are #2136's now. The
pre-rebase branch is preserved at fridah/k3-layerwise-fused-export-prerebase.

What remains here

  1. Decoder layers behind nested wrappersget_homogeneous_hf_decoder_layers unwrapped
    .model then .language_model once each, so it only found layers exactly two wrappers
    deep in that order. K3 keeps its decoder at language_model.model.layers, so the walk
    stopped on the intermediate wrapper and reported the architecture unsupported. Now
    unwraps iteratively, bounded against cycles. Not K3-specific.
  2. Multimodal support — per-layer export refused VLMs. Calibration runs on the extracted
    language model, so the exporter is told which model the checkpoint describes:
    EXPORT_PARENT_ATTR carries the link and the parent is located by identity. Tensor keys
    gain the prefix, the unquantized towers are exported (they were dropped entirely) and
    added to exclude_modules (or a loader reads plain BF16 as NVFP4), and the config
    artifacts come from the parent. The refusal narrows rather than disappears: a VLM whose
    language model is not reachable from the full model is still refused, since the prefix
    would be undefined. Includes a transformers-4 hub-name fallback — also not
    K3-specific
    : any transformers-4 model with a _checkpoint_conversion_mapping got
    in-memory names from per-layer export and hub names from whole-model export.
  3. Offloaded weights read outside their own forward — accelerate materializes a weight in
    its own module's pre-forward hook, so a weight read from a sibling's forward is on meta
    when used. K3's _apply_attn_res does exactly that; a disk-offloaded K3 could not run a
    forward at all.
  4. Recipe — experts-only NVFP4 + FP8 KV for offloaded models, scoped *.experts.* rather
    than *block_sparse_moe* (the broad glob also matches shared_experts.* and
    routed_expert_*_proj, 552 modules the vendor left unquantized), plus its ptq.md row.

Also fixed during the rebase: the recipe shipped checkpoint_dir: /tmp/..., which is
container-local — a run that outlasts its GPU session came back to a wiped manifest and
restarted at layer 0, the exact failure the recipe exists to avoid. Left unset, #2136 derives
<export_path>.layerwise_resume, next to the shards it describes.

Usage

quantize:
  algorithm:
    method: max
    layerwise:
      enable: true
      calib_mutates_weights: false
      export_dir: /tmp/modelopt_layerwise_export   # presence is the switch; value replaced with --export_path
      # checkpoint_dir omitted -> derived as <export_path>.layerwise_resume
python examples/hf_ptq/hf_ptq.py \
    --pyt_ckpt_path  <bf16_ckpt> \
    --recipe         general/ptq/nvfp4_experts_only-kv_fp8_layerwise_export_offload \
    --export_path    <out> \
    --qformat nvfp4 --trust_remote_code --attn_implementation eager \
    --offload_folder <scratch> --max_gpu_memory_gb 140 --max_cpu_memory_gb 1700 \
    --calib_size 256 --batch_size 8 --skip_generate

# Re-running the same command IS the resume path: it reads the manifest beside the
# shards, skips finished layers, and continues.

Testing

tests/gpu/torch/export/test_layerwise_export.py25 passed (24 inherited from #2136,
plus multimodal equivalence: VLM namespace, towers present, config from the parent). That
test is not vacuous: with the parent link stubbed out it fails with "vision tower missing
from the checkpoint"
.

tests/unit/recipe 283 · tests/unit/torch/export 172 · tests/examples/hf_ptq 36 ·
pre-commit clean.

Validated on real models, not only fixtures:

  • Full Kimi-K3 — 93/93 layer shards + tail + index, 1.65 TB, all 247,296 expert
    projections (= 92 × 896 × 3) carrying a calibrated input_scale.
  • Resume across real session kills — produced over three 4-hour GPU sessions; the third
    printed Checkpoint: resuming layerwise calibration from layer 13/93 and skipped the
    finished work.
  • vLLM 0.27.1 accepts the checkpoint: quant_algo=NVFP4, kv_cache_dtype=fp8_e4m3, and
    selects the FLASHINFER_TRTLLM NvFp4 MoE kernel (not the emulation fallback).

Re-run pending after the rebase. The Kimi-K3 numbers above predate it. The multimodal
path was reimplemented against #2136's current exporter rather than cherry-picked, so it
needs one full K3 run to confirm before this leaves draft.

Not validated: generation and accuracy. The checkpoint is 1.65 TB against 1.46 TB of HBM
on 8× B200, and vLLM's cpu_offload_gb is a no-op for this model (80 and 200 give
byte-identical on-device memory). That is a hardware gap, not a checkpoint defect.

Before your PR is "Ready for review"

Additional Information

Draft: depends on #2136 and must not merge before it.

⚠️ Merge-order interaction. #2136 currently refuses multimodal models, and that refusal
is covered there by a unit test and verified on a real Qwen3.6-35B checkpoint. This PR
replaces it with support. Whichever lands second needs that test reconciled; the cleanest
order is #2136 first, then this PR updating the refusal test alongside the feature.

⚠️ Commits are DCO signed off (-s) but not GPG signed — consistent with the #2136 branch,
flagged since the template asks.

@copy-pr-bot

copy-pr-bot Bot commented Aug 19, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 08fb23b6-69e1-441c-8695-0ed57af184ee

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

Fridah-nv and others added 4 commits August 21, 2026 23:01
get_homogeneous_hf_decoder_layers unwrapped .model then .language_model once each,
so it only reached layers exactly two wrappers deep and in that order. Kimi-K3 keeps
its decoder at language_model.model.layers, so the walk stopped on the intermediate
wrapper, returned None, and the architecture was reported unsupported -- which takes
layerwise calibration and per-layer export out of reach for the model that needs them
most. Unwrap iteratively instead, bounded so a cycle cannot hang.

Also re-apply --attn_implementation after model construction. Kimi-K3's remote code
overwrites _attn_implementation to flash_attention_2 unconditionally in __init__,
ignoring the flag; export runs a trace forward, so an unavailable backend fails there
rather than at load. Sub-configs are walked because remote code typically rewrites the
nested text_config, and layer modules hold a reference to the same object. Only applied
when the caller passed the flag explicitly, so nothing changes by default.

Both were found in the previous Kimi-K3 run (PR #2008 workflow) but were never
upstreamed; the branch carrying them was deleted after that PR merged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Fridah-nv <201670829+Fridah-nv@users.noreply.github.com>
(cherry picked from commit 9f86b28)
…d models

Pairs layerwise.export_dir with checkpoint_dir for a PTQ run too large to hold
resident, so an interrupted run resumes without recalibrating or re-exporting
finished layers. That combination is the point for a run that outlasts its GPU
session -- a kill loses at most the in-flight layer.

Scopes experts as '*.experts.*' rather than '*block_sparse_moe*'. On fine-grained
MoE the broader glob also matches shared_experts.*, routed_expert_{up,down}_proj and
routed_expert_norm; on Kimi-K3 that is 552 additional modules the vendor left
unquantized, one of which is an RMSNorm. shared_experts is missed by the narrow glob
because its path contains '_experts.' rather than '.experts.', per fnmatch semantics.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Fridah-nv <201670829+Fridah-nv@users.noreply.github.com>
(cherry picked from commit 02ed6b5)
…rward

accelerate materializes an offloaded weight in that module's pre-forward hook and
returns it to meta in the matching post-forward. A weight read from a *sibling's*
forward is therefore on meta at the moment it is used. Kimi-K3 does exactly this:
_apply_attn_res computes norm.weight.float() * proj.weight.squeeze(0).float() from the
decoder layer's forward, reaching into six children (three call sites in
modeling_kimi_linear.py). The result is

    RuntimeError: Tensor on device meta is not on the expected device cuda:0!

which is why a disk-offloaded K3 could not run a forward at all.

Setting the tensor resident is not enough on its own -- post_forward walks the module's
tensors and pushes every one back to meta, so the hook has to go. Detaching alone is not
enough either: AlignDevicesHook.detach_hook restores each tensor to
original_devices[name] and skips meta, which is precisely what a disk-offloaded param
has, so it would be left on meta. Retargeting original_devices at the execution device
first makes detach materialize the values itself, through accelerate's own code path
rather than a hand-rolled copy.

Safe because these modules' forward is never called -- only their raw .weight is read --
so removing the hook removes nothing that was doing work. The tensors are one row each,
(1, hidden) and (hidden,), so pinning all six on a 93-layer model costs single-digit MB.

Verified on a disk-offloaded tiny Kimi-K3: 10 externally-read params on meta before, 0
after, and the forward completes with finite logits where it previously raised.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Fridah-nv <201670829+Fridah-nv@users.noreply.github.com>
(cherry picked from commit 4c50e13)
The recipe shipped undocumented; the parent branch's docs test now requires a
row and the count kept in step.

Signed-off-by: Fridah-nv <201670829+Fridah-nv@users.noreply.github.com>
@Fridah-nv
Fridah-nv force-pushed the fridah/k3-layerwise-fused-export branch from 521be11 to d4f0d50 Compare August 21, 2026 23:13
@copy-pr-bot

copy-pr-bot Bot commented Aug 21, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://NVIDIA.github.io/Model-Optimizer/pr-preview/pr-2218/

Built to branch gh-pages at 2026-08-21 23:38 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

Calibration runs on the extracted language model, so the exporter has to be told
which model the checkpoint describes. EXPORT_PARENT_ATTR carries that link and
resolve_export_parent() turns it into a key prefix, found by identity so a module
that merely looks like the language model cannot be mistaken for it.

Three things then move into the parent's namespace: tensor keys gain the prefix,
the vision tower and projector are collected in finalize() -- calibration never
saw them and every other pass walks the submodel -- and the quant config's module
references are rewritten, with those towers added to exclude_modules so a loader
does not read plain BF16 as quantized. The config artifacts are written from the
parent, and the VLM path no longer overwrites config.json afterwards, which would
have stripped quantization_config off a finished checkpoint.

The refusal narrows rather than disappears: a VLM whose language model is not
reachable from the full model still cannot be exported, because the prefix would
be undefined and the shards would silently describe the submodel alone.

Reimplemented against the current exporter rather than cherry-picked -- the name
mapper is now built from the export model, since Gemma3-VL stores the decoder at
model.language_model.layers but publishes it as language_model.model.layers, and
a submodel-scoped mapper cannot produce the published name. build_legacy_name_mapper
covers transformers < 5, where save_pretrained rather than the exporter reverses
_checkpoint_conversion_mapping.

The test fails without the parent link: the vision tower is absent entirely.

Signed-off-by: Fridah-nv <201670829+Fridah-nv@users.noreply.github.com>
The recipe shipped checkpoint_dir: /tmp/modelopt_layerwise_ckpt, which is
container-local -- a run that outlasts its GPU session came back to a wiped
manifest and restarted at layer 0, the exact failure this recipe exists to avoid.
Leaving it unset lets hf_ptq derive <export_path>.layerwise_resume, which lives
next to the shards it describes.

Signed-off-by: Fridah-nv <201670829+Fridah-nv@users.noreply.github.com>
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