Skip to content

[OMNIML-6062] Dequantize block-FP8 checkpoints as they load expert-parallel - #2743

Open
shengliangxu wants to merge 5 commits into
shengliangx/hfep-6b-ep-dense-shardingfrom
shengliangx/hfep-6c-fp8-dequant
Open

shengliangxu wants to merge 5 commits into
shengliangx/hfep-6b-ep-dense-shardingfrom
shengliangx/hfep-6c-fp8-dequant

Conversation

@shengliangxu

@shengliangxu shengliangxu commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Note

Re-parented on 2026-10-30 with #2742 onto #3136, #2741 [2/3] (the expert-parallel loader). #2741 was split into three PRs on review: #3131 [1/3], #3136 [2/3] and #2741 [3/3], its post-load refusals. This PR stays on #2742, whose dense-sharding rule it extends. The series first became a tree on 2026-10-27 (this PR was [11/21]).

Base: #2742. The diff below is this PR's alone, and it merges after #2742.

Re-parent: this PR's diff is unchanged, except that its FP8 refusals run inside #3136's checks guard, so a rank refusing alone fails every rank. That includes the per-tensor and unverifiable FP8-config refusals, with their read of the checkpoint's config.json. The PR's 25 commits are condensed into one.

Series layout

What does this PR do?

Type of change: New feature

A native block-FP8 checkpoint, such as DeepSeek-V3, is dequantized to bf16 while it loads expert-parallel, through transformers' FineGrainedFP8Config(dequantize=True). Quantizers attach to bf16 weights.

  • A block-FP8 checkpoint keeps its dense part replicated (moved here from [OMNIML-6061] Load the dense part sharded, and size the load warmup to each rank #2742, where block FP8 did not load at all): DCP, which loads a sharded dense part, cannot dequantize it: it reads the checkpoint as stored. Left to decide, the plan ([OMNIML-6054] Decide how a model is distributed, from the model built on the meta device #2737) replicates it and says so; an explicit fsdp is refused by the decision, naming MODELOPT_FSDP_SIZE=1, and by the loader.

  • Per-tensor FP8 is refused. AutoFP8's per-tensor checkpoints name the same quant_method without a weight_block_size; transformers' dequant would read them as 128x128 blocks and load their weights unscaled, without an error. The load refuses them, before anything loads, as fp8-per-tensor, naming the weight_block_size they lack. A config handed in as transformers' FP8 config object, which fills in a 128x128 block, is read against the checkpoint's own config.json, and refused when the path holds none to read (a Hub ID the load has not made local yet). A tiny per-tensor FP8 Llama, loaded that way on transformers 5.18:

    Weights Largest absolute value
    original 0.079
    loaded as block FP8 448

On transformers older than 5.10.1, the loader also patches that dequantization in three ways:

  • Partial last block. Stock dequantization infers the block size by dividing the weight shape by the scale grid's, so it raises on a weight whose last 128x128 block is partial, such as DeepSeek-V3's kv_a_proj_with_mqa: (576, 7168) with a (5, 56) grid. It also raises on an empty scale grid. The patch tiles each block's scale and crops to the weight, using the checkpoint's weight_block_size.
  • Output dtype. Stock dequantization does not take the destination dtype. The patch emits the destination dtype the loader passes, else the model's dtype.
  • Routed experts. The FP8 quantizer prepends its dequantization to every weight converter, so the fused-experts converter no longer starts with a MergeModulelist. The loader then assigns no per-expert index, and the expert-parallel shard slices each expert's rows instead of whole experts: a DeepSeek-V3 gate_up_proj loads as (256, 32, 7168) and fails calibration. The patch fuses the dequantization into the merge, so the converter starts with a MergeModulelist again and runs as upstream intends.

The patch installs once and survives a re-imported module. From transformers 5.10.1 it keeps upstream's dequantization, which takes the destination dtype and decodes E8M0 scales, and only pads a weight with a partial last block to whole blocks for it, in its own fp8 dtype, since upstream still infers the block from the scale grid: (448, 7168) under a (4, 56) grid would otherwise be scaled in blocks of 112 rows.

The pre-5.10.1 dequantization scales its own fp32 copy of the weight in place, through a view of its whole column blocks, against scales tiled over the rows only, so no full-size fp32 tile is built. It is rebound only once the expert converter's fuse is in, so a refused patch leaves transformers as it was. Against scaling by a full-size fp32 tile, on CPU with one thread:

Weight Peak memory growth: full tile Peak memory growth: row-tiled Time: full tile Time: row-tiled
(18432, 7168) 1014 MiB (8.0 B/elem) 759 MiB (6.0 B/elem) 1.13 s 0.89 s
(7168, 2048) 116 MiB 88 MiB 127 ms 99 ms

Usage

Automatic: load_and_prepare_ep reads the checkpoint's quantization_config and picks this path for quant_method: fp8 with a weight_block_size.

Testing

Before your PR is "Ready for review"

  • Is this change backward compatible?: ✅ Additive; runs only on the expert-parallel load of a block-FP8 checkpoint.
  • If you copied code from any other sources or added a new PIP dependency, did you follow guidance in CONTRIBUTING.md: N/A
  • Did you write any new necessary tests?: ✅
  • Did you update Changelog?: N/A. The feature's entry lands with [OMNIML-6085] [3/3] Run expert-parallel PTQ from hf_ptq.py #2754.
  • Did you get Claude approval on this PR?: ✅ Approved 2026-10-28 at a4609752e39. The 2026-10-29 re-cut only moved its edits of a deleted test file to test_model_load_utils.py (Testing).

Additional Information

Part 11 of 21 of expert-parallel PTQ for Hugging Face MoE models; merges after #2742.

Summary by CodeRabbit

  • New Features
    • Added support for loading models from native block-FP8 checkpoints, converting weights to the model’s configured dtype during loading.
    • Improved handling of partial or empty scale grids, varied tensor shapes, and per-expert weights.
    • Added compatibility with older Transformers versions while preserving built-in behavior in newer versions.

@copy-pr-bot

copy-pr-bot Bot commented Oct 12, 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.

@coderabbitai

coderabbitai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
Walkthrough

The loader now configures dequantization for native FP8 checkpoints. For Transformers versions before 5.10.1, a compatibility patch handles scale grids and expert merging. Unit tests cover dequantization cases, patch behavior, and version gating.

Changes: FP8 checkpoint loading

Layer / File(s) Summary
FP8 dequantization and validation
modelopt/torch/utils/plugins/model_load_utils.py, tests/unit/torch/utils/test_fp8_dequant_patch.py
Adds dequantization support for partial and empty scale grids on Transformers versions before 5.10.1. Tests compare varied block shapes and output dtypes with reference results, and verify that newer versions bypass the patch.
Expert conversion and patch lifecycle
modelopt/torch/utils/plugins/model_load_utils.py, tests/unit/torch/utils/test_fp8_dequant_patch.py
Combines FP8 dequantization with expert merging to preserve per-expert indexing. Tests cover converter ordering, repeated patching, hook wrapping, and restoration.
FP8 loader configuration
modelopt/torch/utils/plugins/model_load_utils.py
Configures FP8 checkpoint loading with FineGrainedFP8Config(dequantize=True), applies the patch with the model dtype, and logs the selection. The MXFP4 configuration remains the alternative branch.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant load_and_prepare_ep
  participant from_pretrained
  participant CompatibilityPatch
  load_and_prepare_ep->>from_pretrained: load FP8 checkpoint with FineGrainedFP8Config(dequantize=True)
  load_and_prepare_ep->>CompatibilityPatch: apply patch with model dtype
Loading
Merge Risk: Moderate · up to 78b38

Loading block-FP8 checkpoints with older Transformers versions can silently produce incorrectly scaled weights in edge cases. Those weights would then be used for calibration and export. Before merging, make the patch reject scale grids that do not match their weights, or document why the current fallbacks are valid.

Pre-merge checks: Passed 5
Check name Status Explanation
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Security Anti-Patterns Passed No listed security anti-pattern was introduced. The PR changes only modelopt/torch/utils/plugins/model_load_utils.py and a unit test; no modelopt/example dependency manifests changed. The added code…
Title check Passed The title names the main change: block-FP8 checkpoints are dequantized as they load under expert-parallel loading. The raw summary and PR objectives both center on the FP8 dequantization patch in load…
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Full details: Security Anti-Patterns

Explanation

No listed security anti-pattern was introduced. The PR changes only modelopt/torch/utils/plugins/model_load_utils.py and a unit test; no modelopt/example dependency manifests changed. The added code contains no torch.load(..., weights_only=False), numpy.load(..., allow_pickle=True), hardcoded trust_remote_code=True, external-input eval()/exec(), or # nosec comment. Existing trust_remote_code handling remains caller-configurable with a default of False.

Full details: Title check

Explanation

The title names the main change: block-FP8 checkpoints are dequantized as they load under expert-parallel loading. The raw summary and PR objectives both center on the FP8 dequantization patch in load_and_prepare_ep, which matches the title. The title is specific, uses the ticket tag, and is a single sentence of 8 words.

Finishing Touches
Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
Fix failing CI checks
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

@shengliangxu
shengliangxu added this pull request to stack #2755 October 12, 2026 00:57
@codecov

codecov Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.54839% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.86%. Comparing base (aea6150) to head (adc762a).

Files with missing lines Patch % Lines
modelopt/torch/utils/plugins/model_load_utils.py 93.54% 6 Missing ⚠️
Additional details and impacted files
@@                            Coverage Diff                            @@
##           shengliangx/hfep-6b-ep-dense-sharding    #2743      +/-   ##
=========================================================================
+ Coverage                                  61.83%   61.86%   +0.03%     
=========================================================================
  Files                                        649      649              
  Lines                                      72969    73059      +90     
=========================================================================
+ Hits                                       45117    45195      +78     
- Misses                                     27852    27864      +12     
Flag Coverage Δ
unit 61.86% <93.54%> (+0.03%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-6b-ep-dense-sharding branch from 6eab845 to d6cb219 Compare October 12, 2026 02:24
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-6c-fp8-dequant branch from f354b3b to ed1ebbf Compare October 12, 2026 02:35
@github-actions

github-actions Bot commented Oct 12, 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-2743/

Built to branch gh-pages at 2026-10-27 00:04 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test ed1ebbf

@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-6b-ep-dense-sharding branch from d6cb219 to a5aecef Compare October 12, 2026 08:39
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-6c-fp8-dequant branch from ed1ebbf to 6db2eb0 Compare October 12, 2026 08:39
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 6db2eb0

@shengliangxu shengliangxu changed the title [8/19] Dequantize block-FP8 checkpoints as they load expert-parallel [OMNIML-6062] [8/19] Dequantize block-FP8 checkpoints as they load expert-parallel Oct 12, 2026
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-6b-ep-dense-sharding branch from a5aecef to e8384c3 Compare October 13, 2026 00:53
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-6c-fp8-dequant branch from 6db2eb0 to 6bcb1ca Compare October 13, 2026 00:53
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-6b-ep-dense-sharding branch from e8384c3 to ffbb850 Compare October 13, 2026 01:32
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-6c-fp8-dequant branch from 6bcb1ca to 14f1610 Compare October 13, 2026 01:32
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 14f1610

@shengliangxu
shengliangxu removed this pull request from stack #2755 October 13, 2026 17:42
@shengliangxu
shengliangxu added this pull request to stack #2775 October 13, 2026 17:43
@shengliangxu shengliangxu changed the title [OMNIML-6062] [8/19] Dequantize block-FP8 checkpoints as they load expert-parallel [OMNIML-6062] [8/20] Dequantize block-FP8 checkpoints as they load expert-parallel Oct 13, 2026
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-6c-fp8-dequant branch from 14f1610 to f647855 Compare October 13, 2026 19:38
@shengliangxu
shengliangxu removed this pull request from stack #2775 October 13, 2026 19:38
@shengliangxu
shengliangxu added this pull request to stack #2781 October 13, 2026 19:40
@shengliangxu shengliangxu changed the title [OMNIML-6062] [8/20] Dequantize block-FP8 checkpoints as they load expert-parallel [OMNIML-6062] [9/21] Dequantize block-FP8 checkpoints as they load expert-parallel Oct 13, 2026
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test f647855

@shengliangxu
shengliangxu removed this pull request from stack #2781 October 14, 2026 00:13
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-6c-fp8-dequant branch from f647855 to a40c380 Compare October 14, 2026 00:14
@shengliangxu
shengliangxu added this pull request to stack #2786 October 14, 2026 00:14
@shengliangxu shengliangxu changed the title [OMNIML-6062] [9/21] Dequantize block-FP8 checkpoints as they load expert-parallel [OMNIML-6062] [10/22] Dequantize block-FP8 checkpoints as they load expert-parallel Oct 14, 2026
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-6c-fp8-dequant branch from a40c380 to d407931 Compare October 14, 2026 01:47
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

@kevalmorabia97, an update: the GPU path you couldn't run passed at c44d5c1, on GB200 with torch 2.11.0a0 and transformers 5.9.0. That commit includes the packed-FP4 fix.

Test file at c44d5c1 2 GPUs 4 GPUs
tests/gpu/torch/utils/test_expert_parallel_load.py 8 passed, 4 skipped (123 s) 12 passed (144 s)

This head, 54430ac, is only a rebase onto #2742's two new commits, and this PR's own diff is unchanged. The tied-head question above is still open: a traceback and your torch, accelerate and safetensors versions would let me reproduce it.

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 54430ac

@kevalmorabia97 kevalmorabia97 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I reviewed this independently, with a focus on block-FP8 dequant correctness, per-rank expert and dense slices, and memory during load. It looks good to me at 54430ac.

  • FP4 experts on transformers 5.9 (fixed in 54430ac). At e5ded19, the pre-5.10.1 override read packed-FP4 (int8) expert weights as plain integers and returned them at half width, with no error. Transformers 5.9's own dequant does unpack FP4, and DeepSeek-V4 stores its routed experts that way under a block-FP8 config. At this head, any weight that is not fp8 goes to transformers' own dequant. I checked on 5.9.0: a (256, 512) FP4 weight now matches stock up to bf16 rounding.
  • The dequant matches a blockwise reference for a partial last block, a weight narrower than one block, and E8M0 scales.
  • Under expert parallelism, each expert's weight and scale get the same expert index and are taken whole, and an expert held on another rank drops both. The dense part stays replicated for block FP8, both in the plan and in the loader's refusal, so a scale never meets a sharded tensor.
  • Memory is about 6 B/elem per weight on the pre-5.10.1 path, and the 5.10.1+ path pads in fp8.
  • Mutation checks: each of four deliberate breaks fails tests (the expert fuse, block-size choice, per-tensor refusal, partial-column scaling).

Ran on CPU (transformers 5.9.0, torch 2.12): test_fp8_dequant_patch, test_ep_loader, test_model_load_utils and test_ep_load_warmup at 54430ac (204 passed).

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test a73d523

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

@kevalmorabia97 thanks for the re-review and the mutation checks. Your green run answers my tied-head question. This head, a73d523, only rebases onto #2742's docstring commit, so this PR's own diff is unchanged.

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 09e3d73

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test c884757

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 9c917ae

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 7192e05

@juhi10071998

Copy link
Copy Markdown
Contributor

Blocker (probable): the block-FP8 path is admitted on transformers 5.7, where none of this PR's fixes install.

load_and_prepare_ep only requires transformers 5.7 (require_transformers("5.7", ...), model_load_utils.py), and this PR adds "fp8" to _EP_SOURCE_FORMATS with no further version check. But FineGrainedFP8HfQuantizer.update_weight_conversions and Fp8Dequantize._dequantize_one first appear in transformers 5.8 (checked v5.7.0 vs v5.8.0: v5.7.0 has neither, v5.8.0 has both). On 5.7 the _dequantize_one override is bound but never called and the expert-converter fuse is skipped because the hook does not exist.

What 5.7 does instead: get_weight_conversions appends one generic ["weight$", "weight_scale_inv", ...] dequant converter after the model's own converters. rename_source_key stops at the first matching converter, and the expert converter's pattern (mlp.experts.*.gate_proj.weight) is searched without an end anchor, so it claims the expert weights first. The likely result is that routed experts are merged as raw fp8 and cast to bf16 with no scales applied, and their *_scale_inv keys are renamed to a target the model lacks and dropped.

Who hits it silently: a block-FP8 MoE whose dimensions are all multiples of 128 (e.g. a Qwen3-MoE FP8 checkpoint). DeepSeek-V3 fails loudly instead, since 5.7's inline dequant raises on (576, 7168).

Tests/GPU runs here cover 5.5, 5.9 and 5.18 only; the author's replies name 5.8-5.10.0 as the releases the fuse installs on, and 5.7 has not been discussed. I traced this from the transformers sources and did not run it, so confidence is medium.

Suggested fix: require transformers >= 5.8 for quant_method == "fp8" inside the checks guard, or add a 5.7 test showing it loads correctly.

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 7c83230

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/claude review

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

@juhi10071998 thanks, confirmed and fixed in 7c83230. transformers 5.7.0 has neither Fp8Dequantize._dequantize_one nor FineGrainedFP8HfQuantizer.update_weight_conversions; 5.8.0 has both. On 5.7, this PR's override is never called and its fuse doesn't install, as you traced.

The expert-parallel load now refuses a block-fp8 checkpoint below transformers 5.8, inside the load's checks guard, so every rank refuses it before anything loads. test_a_block_fp8_checkpoint_needs_the_transformers_that_dequantizes_its_experts presents 5.7.0 and fails without the check. Unquantized checkpoints keep the 5.7 floor.

raw = json.load(f)
except (OSError, ValueError):
return None
for c in (raw, raw.get("text_config"), raw.get("llm_config")):

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.

[SUGGESTION] raw.get(...) is evaluated eagerly to build the tuple, so a config.json whose top-level JSON is not an object escapes the guard below it. json.load accepts [], "x" or 3 as valid JSON, and the isinstance(c, dict) check on the next line only protects the inner levels — the two raw.get calls run first and raise AttributeError: 'list' object has no attribute 'get'.

Why it matters: the function is deliberately defensive (except (OSError, ValueError) → None) precisely so an unreadable or malformed config.json degrades to the "fp8-unverified" refusal with its actionable message. A top-level non-object skips that and surfaces a bare AttributeError from inside _ckpt_quant_method, on the path that now runs for every expert-parallel load that reads an FP8 config object — the one place the user has no hint what went wrong. Low likelihood, but the fix is one line and keeps all malformed-config cases on the same route.

Suggested change
for c in (raw, raw.get("text_config"), raw.get("llm_config")):
if not isinstance(raw, dict):
return None
for c in (raw, raw.get("text_config"), raw.get("llm_config")):

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Taken in 5000023: _config_json_quantization now returns None for a config.json whose top level isn't a JSON object, so an FP8 config object meets the same fp8-unverified refusal as with an unreadable file. test_an_fp8_config_object_is_read_against_the_checkpoints_own_config gained a config-json-not-an-object row ([]), which fails without the check with the AttributeError you describe.

@claude

claude Bot commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

Claude review — block-FP8 dequant on the expert-parallel load

Scope: full review (the trigger comment carried no scoping instructions). All 3 changed files reviewed against shengliangx/hfep-6b-ep-dense-sharding: modelopt/torch/utils/plugins/model_load_utils.py (+339/-8), tests/unit/torch/utils/test_model_load_utils.py (+413/-16), tests/unit/torch/utils/test_ep_loader.py (+81/-21). For composition I also read the _refused_together helper (:509), the sharded load's own quantized-checkpoint refusal (:586) that the new elif block_fp8 comment leans on, the dtype resolution at :2566, and the _decide_distribution call site at :1746.

Findings: CRITICAL: 0, IMPORTANT: 0, SUGGESTION: 1

What changed since the last approved round

The source diff is the previously reviewed arithmetic plus four deltas, all of which I re-derived:

  • The FP8 refusals moved inside [OMNIML-6060] [2/3] Load an MoE expert-parallel: shard the experts at load time #3136's _refused_together("the expert-parallel load's checks", alone=False) guard. Verified this is right rather than merely compiling: all four refusals (require_transformers("5.8"), the fsdp-dense refusal, fp8-per-tensor, fp8-unverified) are version/config/plan-derived, so they are already rank-identical; wrapping them only removes the window where a rank that read a different config refuses alone. The guard re-raises via dist.all_reduce(label, refuse, combine=...), which every rank reaches, so no rank is left in a collective. quant_method is read inside the with and used at :2628 outside it — same function scope, no with-scoping trap.
  • require_transformers("5.8", ...) for block FP8 sits after the unconditional "5.7" floor and before every other FP8 check, so a 5.7 run gets the specific message rather than the generic format refusal. Consistent with the pre-5.10.1 branch's assumption that vars(Fp8Dequantize)["_dequantize_one"] exists at the gate.
  • print_rank_0 replaces the is_master() print. Issues no collective; print_rank_0/warn_rank_0 are already imported at :72.
  • The non-fp8 passthrough (kevalmorabia97's round). quantized.dtype not in fp8_dtypes → stock(self, quantized, scales).to(out_dtype) is the correct shape for packed FP4: fp8_dtypes is built from float8_e4m3fn/e5m2/e4m3fnuz/e5m2fnuz only, so float4_e2m1fn_x2 and int8 both miss it, and the .to(out_dtype) keeps the model-dtype contract the override exists for. stock is None (5.5) raising rather than returning the half-width tensor is the right failure direction.

Verified fresh this round (no finding; recorded so it isn't re-derived)

  • The in-place column scaling really is in place. q[..., : whole * block_n] is a stride-1-in-the-last-dim slice, so unflatten(-1, (whole, block_n)) is viewable (computeStride permits splitting a contiguous dim regardless of the outer strides) — mul_ writes through to q, not to a reshape copy. If that slice ever became non-viewable the mutation would silently land on a temporary and the whole-block columns would load unscaled, so it is worth having pinned: whole-blocks, partial-last-blocks and narrower-than-a-block (where whole == 0, i.e. unflatten(-1, (0, 4))) all go through this line and all compare against the blockwise reference.
  • The partial-column tail broadcasts correctly in every reachable case. whole < scale_cols can only hold with scale_cols - whole == 1 when the grid is the configured block's ceil grid, and with an exact split whole == scale_cols and the branch is skipped — so s[..., whole:] is always a single column broadcasting over the remainder.
  • The >= 5.10.1 bypass clause now gates on block alignment, not grid divisibility — the inverse of the rule that was the CRITICAL two rounds ago. (448, 7168) under a (4, 56) grid hits the pad path instead of stock's block_m = 112, and partial-dividing-the-grid ((192, 256) on (2, 2)) is the regression test for exactly that class.
  • The pad path cannot be handed work it does not implement. quantized.view(torch.uint8) is legal on a non-contiguous fp8 tensor (same itemsize, no stride constraint), F.pad fills bit-zero = fp8 0.0 so the padded tail dequantizes to zeros and is cropped off, and scales passes through untouched so upstream's E8M0 decode is unaffected. FP4 and non-2-D inputs are excluded before the geometry is read.
  • _DequantThenMerge is idempotent under a second update_weight_conversions. The second call prepends a fresh Fp8Dequantize, and because the fused op is a MergeModulelist the ops[0]/ops[1] match fires again and collapses it — the new outer dequant does the work and the inner one is dropped, so operations[0] can never be left an Fp8Dequantize. That is the failure mode the whole patch exists to prevent.
  • The dense-layout rule and its two enforcement points agree. block_fp8 is computed after ep_size is resolved from "auto" (:1588 precedes :1601), so it is never compared against the string; "fp8-per-tensor" / "fp8-unverified" deliberately miss it and are caught by the load's own refusals; and the elif block_fp8 branch's comment ("the FSDP2 load refuses a quantized checkpoint itself") checks out against :586, which refuses any quantization_config on the sharded route — so the ep_size=1 hole the comment waves at is genuinely closed elsewhere.
  • No new rank divergence. _patch_finegrained_fp8_dequant runs unconditionally on every rank for quant_method == "fp8" and can only raise on import/attribute conditions that are identical across ranks; _config_json_quantization reads a file every rank can read (or that every rank fails to read the same way), and in resolve_distribution_plan it runs under dist.broadcast_from, so only the deciding rank touches it at all.

Suggestion (non-blocking, 1 inline)

What reads well

  • _blk refusing to guess rather than deriving a block from divisibility, with the two misleading cases (576 % 5, 9 % 3) written into the comment and into the parametrization (grid-divides-the-rows) — a divide-the-shape rule passes that row, which is why it is the one that pins the fix.
  • Rebinding Fp8Dequantize._dequantize_one only after the converter fuse is in, so a refused patch leaves transformers byte-identical — and test_an_expert_converter_it_cannot_fuse_stops_the_load asserting that over the full _PATCH_TARGETS tuple rather than one attribute.
  • The three FP8 refusals each naming the specific missing thing (weight_block_size, a readable config.json, MODELOPT_FSDP_SIZE=1) instead of a generic "cannot dequantize fp8", and landing ahead of ensure_local_checkpoint so a refused load downloads nothing.
  • The replicated-dense-part test asserting the absence of MODELOPT_EP_DEGREE and the presence of unquantized copy as functions of the parametrization, so the block-FP8 and export-layout remedies cannot drift into each other.
  • test_the_dequantize_config_keeps_the_checkpoints_block_size pinning the merge_quantization_configs behaviour the entire block-size story depends on — an upstream change there would otherwise show up as silently mis-scaled weights, not a failing test.

Risk

Low. Additive and reached only on the expert-parallel load of a block-FP8 checkpoint; every path that could load wrong weights silently (empty grid, unplaceable grid, non-fp8 dtype, per-tensor scales, an unfusable expert converter) raises instead, and each is pinned by a test the author reports fails without the fix. The remaining item is a robustness one-liner on a malformed-config path.

🤖 Generated with Claude Code

@claude claude 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.

Claude review passed — no blocking issues found. LGTM

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 5000023

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 80cabec

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 76e0338

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 7540a2e

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test b1cec70

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 6f5391d

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 8787a4d

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test c032300

A native block-FP8 checkpoint, such as DeepSeek-V3's, is dequantized to
bf16 while it loads expert-parallel, through transformers'
FineGrainedFP8Config(dequantize=True); on transformers older than
5.10.1 the loader patches that dequantization for a partial last block,
the destination dtype and the routed experts' fused converter. A
block-FP8 checkpoint keeps its dense part replicated, since DCP reads it
as stored. A per-tensor FP8 checkpoint, which names no weight_block_size,
is refused, and so is an FP8 config object whose checkpoint has no
config.json to say which it is.

This is the PR's 25 commits in one, re-parented with #2742 onto the
expert-parallel loader's [2/3]: the FP8 refusals, with their read of the
checkpoint's config.json, run inside the loader's checks guard, so a rank
refusing alone fails every rank.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Below transformers 5.10.1 the block-FP8 override replaced the dequant
for every weight. From 5.9, transformers' own dequant unpacks packed FP4
(float4_e2m1fn_x2, or int8) with [1, 32] scales, as DeepSeek-V4 stores
its routed experts under a block-FP8 config; the override read each
packed byte as one value and returned the weight at half its width,
without an error. It now hands any weight that is not fp8 to
transformers' dequant, in the model dtype, as the 5.10.1 wrapper does,
and the docstring no longer says these releases cannot unpack FP4.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
test_fp8_dequant_patch.py covers model_load_utils' block-FP8 dequant
patch alone, so its tests join that module's test file, as
CONTRIBUTING.md asks, as one section with their fixtures, marks and
parametrization. Its module-level skip on a torch without float8_e4m3fn
becomes a mark on its tests.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The expert-parallel load takes transformers 5.7, but the hooks block FP8
is dequantized through, Fp8Dequantize._dequantize_one and the FP8
quantizer's update_weight_conversions, first appear in 5.8. On 5.7 the
experts' converter claims their fp8 weights before the dequant can, so a
block-FP8 MoE whose dimensions divide into whole blocks (Qwen3-MoE's
FP8, say) would load its experts unscaled. The load now refuses block
FP8 below 5.8, before anything loads.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
… block

_config_json_quantization returns None for a config.json it cannot read,
so an FP8 config object meets the load's fp8-unverified refusal. A
config.json holding valid JSON other than an object reached .get first
and raised AttributeError instead; it now takes the same route.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 939bf6e

This branch has not been deployed

No deployments
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.

5 participants