Skip to content

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

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

shengliangxu wants to merge 14 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

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 [10/21], 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 ([5/21]) 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. 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 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, 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

  • New tests/unit/torch/utils/test_fp8_dequant_patch.py, on CPU:
    • the patched dequant against a blockwise reference, a weight narrower than one block included;
    • block size from the scale grid or the DeepSeek default;
    • the requested output dtype;
    • an empty scale grid refused for a non-empty weight, rather than passing it through unscaled;
    • idempotent installs that keep the latest dtype;
    • no double-wrapping of a re-imported module;
    • a clean undo;
    • transformers 5.10.1 keeping its own dequant, padded for every partial-block weight, a grid-divisible one included ((192, 256) on a (2, 2) grid);
    • the fused expert converter, run on a real fused-experts converter: it starts with a MergeModulelist, and per-expert block-FP8 weights come out as the blockwise reference. It fails without the fusion, and if the fused op skips the dequantization or the merge. Skipped on transformers 5.5, which has no update_weight_conversions.
  • Mutation-checked: each test fails on at least one deliberate break of the code.
  • The block-size cases join the blockwise-reference table, and dtype and version cases that took the same branch as a kept one are dropped.
  • A refused fuse leaves transformers' dequant as it was.
  • The block-FP8 dense layout: the loader refuses a plan that shards the dense part (test_ep_dense_sharding.py); the decision replicates it, or refuses an explicit fsdp, naming MODELOPT_FSDP_SIZE=1 (test_resolve_distribution_plan.py).
  • Per-tensor FP8 refused before anything loads and read as fp8-per-tensor (test_ep_loader.py; both rows fail without the fix); the block-FP8 configs in the tests carry their weight_block_size.
  • Locally on this revision, the stack rebased on main 0638a27: pre-commit over the whole PR against main, at every PR of the stack; each fix's own tests at its PR; and at the stack's tip the unit tests of the changed areas (tests/unit/torch/utils, export, models, the fused-experts quantization tests and the hf_ptq example tests) on transformers 5.18 and 5.5 and on torch 2.8. They pass apart from the diffusers-export and GGML tests, which fail the same way on main in that environment. CI runs the full unit suite.
  • The GPU results here were measured on earlier revisions of this branch; this revision's GPU tests run in CI only.
  • On GPUs: DeepSeek-V3 (block FP8) loads, calibrates and exports expert-parallel through this path (see [21/21]).

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 [21/21].
  • Did you get Claude approval on this PR?: ❌ Not yet.

Additional Information

Part 11 of 21 of expert-parallel PTQ for Hugging Face MoE models; merges after [10/21].

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.

Priority: Normal

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 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 69.57% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 23 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 5 passed
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 during expert-parallel loading. The raw summary confirms that load_and_prepare_ep now requests FineGrainedFP8Config(dequantize=Tr…
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 during expert-parallel loading. The raw summary confirms that load_and_prepare_ep now requests FineGrainedFP8Config(dequantize=True) for FP8 checkpoints and applies a Transformers compatibility patch. The title is specific and matches the changeset. The bracketed ticket and series tags ([OMNIML-6062] [11/21]) are conventional and do not reduce clarity.

✨ Finishing Touches: 💡 2
📝 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

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

@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.
⚠️ Please upload report for BASE (shengliangx/hfep-6b-ep-dense-sharding@4ec4f1a). Learn more about missing BASE report.

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.70%           
========================================================================
  Files                                            ?      649           
  Lines                                            ?    72887           
  Branches                                         ?        0           
========================================================================
  Hits                                             ?    44972           
  Misses                                           ?    27915           
  Partials                                         ?        0           
Flag Coverage Δ
unit 61.70% <93.54%> (?)

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-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-25 06:17 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-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-6c-fp8-dequant branch from 6db2eb0 to 6bcb1ca Compare October 13, 2026 00:53
@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

/ok to test 6805a2b

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/claude review

# The checkpoint formats load_and_prepare_ep dequantizes as it loads, beside unquantized ones (None).
# mxfp4 is for experts stored in the standard layout: GPT-OSS's are transposed, which the load refuses.
_EP_SOURCE_FORMATS = frozenset({None, "mxfp4"})
_EP_SOURCE_FORMATS = frozenset({None, "fp8", "mxfp4"})

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.

[IMPORTANT Compatibility] "fp8" here admits more checkpoints than the new path can dequantize.

_ckpt_quant_method returns the raw quantization_config.quant_method string, and "fp8" is not exclusive to block FP8. The llm-compressor / vLLM per-tensor FP8 layout — {"quant_method": "fp8", "activation_scheme": "static", "ignored_layers": [...]}, with scalar weight_scale per linear and no weight_block_size — carries exactly the same quant_method. Those checkpoints are common on the Hub.

Before this change such a checkpoint got the actionable refusal two hunks below:

The expert-parallel load cannot dequantize a 'fp8' checkpoint: it loads unquantized checkpoints and dequantizes mxfp4. Start from an unquantized copy of the model.

Now it is accepted, gets FineGrainedFP8Config(dequantize=True) with the default (128, 128) block, and fails somewhere inside transformers' block-FP8 loader — in the best case on scale_rows, scale_cols = scales.shape[-2:] in the new override, which raises a bare ValueError: not enough values to unpack for a 0-d/1-element weight_scale; more likely on missing/unexpected weight_scale_inv keys. Either way the user loses a message that told them what to do.

The distinguishing bit is in the config the loader already has. Suggest narrowing the admission test to block FP8 — e.g. treat quant_method == "fp8" without a weight_block_size in the checkpoint's quantization_config as an unhandled format, so it keeps falling into the existing quant_method not in _EP_SOURCE_FORMATS refusal ("...dequantizes fp8 (blockwise), mxfp4"). That also keeps _decide_distribution's new fp8 clause at line 1297 from refusing dense="fsdp" with a block-FP8 rationale for a per-tensor checkpoint.

@shengliangxu shengliangxu Oct 25, 2026 •

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.

Fixed in 3c17f8a: _ckpt_quant_method now reports an fp8 checkpoint without a weight_block_size as fp8-per-tensor, so the expert-parallel load refuses it, before anything loads, with the format message ("cannot dequantize a 'fp8-per-tensor' checkpoint ... Start from an unquantized copy of the model"), and the decision's block-FP8 rule no longer applies to it. It was worse than a crash: on transformers 5.18 a tiny Llama saved in AutoFP8's per-tensor layout ({"quant_method": "fp8", "activation_scheme": "static"}, a scalar weight_scale and input_scale per projection) loads through FineGrainedFP8Config(dequantize=True) without an error: the dequant finds no weight_scale_inv, passes the FP8 weights through unscaled (max |w| 448 where the original has 0.079), and the scales only show up as unexpected keys in the load report. Per-tensor rows in the format-refusal and _ckpt_quant_method tables fail without the fix; the block-FP8 configs in the tests now carry their weight_block_size.

if quant_method == "fp8":
from transformers import FineGrainedFP8Config

from_pretrained_kwargs["quantization_config"] = FineGrainedFP8Config(dequantize=True)

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] This constructs a fresh FineGrainedFP8Config, so the checkpoint's own weight_block_size only reaches the dequant if transformers' AutoHfQuantizer.merge_quantization_configs happens to special-case FineGrainedFP8Config (keeping the checkpoint's config and copying only the loading attributes, as it does for the dequantize flag on Mxfp4Config). If a release instead takes the passed config wholesale, self.hf_quantizer.quantization_config.weight_block_size that both new branches read (lines 1676, 1721) is the default (128, 128), not the checkpoint's.

DeepSeek-V3 is 128x128, so neither the unit tests (which build the op directly from a SimpleNamespace) nor the GPU run can tell the two apart. For a checkpoint with a different block the consequence is at least an unexplained ValueError: a scale grid of N fits neither the configured block 128 nor an even split of D on any weight with a partial last block — the _blk division fallback silently rescues only the block-aligned ones.

Reading it off the config you already have makes it independent of the merge semantics and one line:

if quant_method == "fp8":
    from transformers import FineGrainedFP8Config

    _qc = getattr(_ep_config(hf_config), "quantization_config", None) or getattr(
        hf_config, "quantization_config", None
    )
    _block = (
        _qc.get("weight_block_size") if isinstance(_qc, dict) else getattr(_qc, "weight_block_size", None)
    )
    fp8_config = FineGrainedFP8Config(dequantize=True, **({"weight_block_size": _block} if _block else {}))
    from_pretrained_kwargs["quantization_config"] = fp8_config

(Also worth asserting in the docstring/comment that this is where the block comes from — the _patch_finegrained_fp8_dequant docstring says "block size from the real weight_block_size", which is only true given that plumbing.)

@shengliangxu shengliangxu Oct 25, 2026 •

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.

Pushing back: the checkpoint's weight_block_size already reaches the dequant on every release this path runs on. get_hf_quantizer sets config.quantization_config = AutoHfQuantizer.merge_quantization_configs(<the checkpoint's quantization_config>, <ours>), which keeps the checkpoint's config and copies only our loading attributes (dequantize, and from 5.8 modules_to_not_convert), since FineGrainedFP8Config is in LOADING_ATTRIBUTES_CONFIG_TYPES; the quantizer whose config both branches read is built from that merged config. The merge is the same in every release from 5.6 to 5.19. On 5.18, merging {"quant_method": "fp8", "weight_block_size": [64, 64]} with FineGrainedFP8Config(dequantize=True) gives weight_block_size [64, 64] and dequantize True, and a tiny Llama saved with 64x64 blocks loads with the dequant reading (64, 64) and matches the original to FP8 rounding (max error 0.0027). A block set on our config would be dropped by that merge, so plumbing it would be dead code. A comment at the call now says where the block comes from (3c17f8a).

" Set MODELOPT_EP_DEGREE to the world size if it divides the expert count, or "
"MODELOPT_FSDP_SIZE=1 if the dense part fits one GPU."
)
elif ep_size > 1 and _ckpt_quant_method(hf_config) == "fp8":

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] Two things about this clause.

1. The ep_size > 1 gate leaves dense="auto" able to pick a plan no loader honors. The reason given — a sharded dense load cannot dequantize — has nothing to do with expert parallelism: _load_sharded refuses any checkpoint with a quantization_config (line 477, "The FSDP2 load cannot dequantize this ... checkpoint; load it unsharded"). With ep_size == 1 (e.g. MODELOPT_EP_DEGREE=1, or an expert count that doesn't divide the world) unshardable stays None, so too_big makes dense = "fsdp" — and test_a_dense_part_the_expert_parallel_load_cannot_shard_is_replicated now asserts exactly that for the block-FP8 config (resolve_distribution_plan(..., ep_size=1)[0] == DistributionPlan(4, dense="fsdp")). The user then gets told to "load it unsharded" for a model the decision just judged too big to replicate: an impossible instruction, where replicate was at least attemptable. Dropping ep_size > 1 and (and widening the condition to "the checkpoint is quantized at all") makes the one decision point consistent with what both loaders accept.

2. "F8_E8M0 scales" is a narrower rationale than the real one, and it is repeated four times (this message, the comment above it, the load_and_prepare_ep docstring, the NotImplementedError at line 2166). DCP doesn't fail on the scale dtype — the FSDP2/DCP route reads tensors as stored and has no dequantization step at all, which is why line 477 rejects every quantized checkpoint regardless of scale format. It's also confusing next to the _patch_finegrained_fp8_dequant docstring, which says the override deliberately does not decode E8M0: a reader comes away thinking E8M0 is the blocker in both places when it is the blocker in neither. Something like "a sharded load reads tensors as stored and cannot dequantize them" would be true for every version and every scale layout, and still names the remedy.

@shengliangxu shengliangxu Oct 25, 2026 •

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.

Fixed in 3c17f8a, point 2: the comment, the decision's reason, the loader's docstring and its refusal now say that Distributed Checkpoint, which loads a sharded dense part, reads a block-FP8 checkpoint as stored, with no dequantization; none names a scale format any more (DeepSeek-V3's scales are fp32). Postponed, point 1: at EP 1 the plan goes to load_pretrained's FSDP2 load ([5/21], [6/21]), whose own refusal covers every quantized format, block FP8 or not. Having [5/21]'s decision replicate any quantized checkpoint there changes the non-expert-parallel plan for every format, so it belongs with that route as a follow-up. Dropping the ep_size > 1 gate for 'quantized at all' would also refuse, at EP > 1, checkpoints whose dense part is stored unquantized, such as MXFP4, which quantizes only the experts.

@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 — block-FP8 dequant on the expert-parallel load

Scope: full review (the trigger comment carried no scoping instructions). All 5 changed files reviewed against the new base shengliangx/hfep-6b-ep-dense-sharding: modelopt/torch/utils/plugins/model_load_utils.py (+244/-5) plus the four test files. I also read _decide_distribution whole, the _route/_load_sharded guards at :410-:480 (where a sharded load's refusal of quantized checkpoints lives), and _ckpt_quant_method at :1922, since the dense-layout decision moved into this PR.

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

Resolved since the last round

  • The full-size fp32 scale tile is gone. repeat_interleave now tiles over the rows only, giving a (rows, scale_cols) temporary — ~4 MB for DeepSeek-V3's (18432, 7168) MLP weights instead of 528 MB — and the whole-column-block unflatten(-1, (whole, block_n)).mul_(...) is a genuine view of q, so the scaling really is in place. The measured 6.0 B/elem in the PR table matches q (fp32) + the bf16 result. The partial-last-column fallback and the whole == 0 case (a weight narrower than one block) both broadcast correctly.
  • The converter fuse is now idempotent in both directions. Because _DequantThenMerge is a MergeModulelist, the isinstance(ops[1], _MergeModulelist) test matches a [Fp8Dequantize, _DequantThenMerge, Concatenate] list and collapses it back to one op, dropping the duplicate prepend rather than skipping the fuse and leaving operations[0] an Fp8Dequantize.
  • The master print reports dtype instead of a hardcoded bf16.

Most impactful finding

[IMPORTANT Compatibility] _EP_SOURCE_FORMATS admits more than this path dequantizes (line 1919). quant_method: "fp8" is not exclusive to block FP8 — the llm-compressor / vLLM per-tensor layout (activation_scheme: static, scalar weight_scale, no weight_block_size) reports the same string, and those checkpoints are common on the Hub. They used to get the actionable "Start from an unquantized copy of the model" refusal; now they are handed FineGrainedFP8Config(dequantize=True) with a default 128x128 block and die inside transformers' block-FP8 loader — at best on scale_rows, scale_cols = scales.shape[-2:] with a bare "not enough values to unpack". The config the loader already holds distinguishes the two: gating admission on weight_block_size being present keeps the clear refusal for the rest.

Suggestions (non-blocking)

  • The checkpoint's weight_block_size may not reach the dequant (line 2251). A fresh FineGrainedFP8Config(dequantize=True) only preserves it if transformers' merge_quantization_configs special-cases this config class the way it does Mxfp4Config; otherwise both branches read the default (128, 128). DeepSeek-V3 is 128x128, so neither the unit tests nor the GPU run can tell. Plumbing the block through explicitly makes the docstring's "block size from the real weight_block_size" true by construction.
  • The new unshardable clause is gated on ep_size > 1, and its stated reason is narrower than the real one (line 1297). A sharded dense load refuses every quantized checkpoint (:477), independent of expert parallelism — so at ep_size == 1 dense="auto" still picks fsdp for a block-FP8 checkpoint (the updated test asserts it) and the user is told to "load it unsharded" for a model just judged too big to replicate. Separately, "F8_E8M0 scales" now appears as the rationale in four places, including two user-facing messages, while the actual blocker is that a sharded load reads tensors as stored and has no dequantization step at all.

What reads well

  • The arithmetic in both version branches holds up against a careful re-derivation: s[..., :whole, None] really does slice the scale columns (the Ellipsis absorbs the leading dims and rows), the row tile is always long enough under both _blk branches, and scale_cols - whole can only ever be 0 or 1, so the partial-block fallback cannot silently drop columns. The in-place path also leaves the caller's tensor alone (to(float32, copy=True)).
  • _blk trusting the configured block over division, with 576 % 5 and 9 % 3 both named as traps, plus the grid-divides-the-rows case that would pass under the naive rule.
  • The 5.10.1+ wrapper's bypass predicate gates on block alignment, not grid divisibility, which is the fix that closed the last round's CRITICAL; partial-dividing-the-grid ((192, 256) on a (2, 2) grid) pins it. Double-wrapping on a re-imported module is harmless there too, since the inner wrapper sees an already-padded fp32 tensor and falls straight through to stock.
  • test_undoing_the_patch_restores_stock_transformers diffing vars(cls) to assert the patch rebinds only its three targets, which is what makes test_an_expert_converter_it_cannot_fuse_stops_the_load meaningful. Both refusal paths fail loud on exactly the states 5.9's tensor_idx bug produced, and the patch can only raise identically on every rank, so no collective divergence.
  • The new load_and_prepare_ep guard sits before _resolve_checkpoint_dir, so a refused plan downloads nothing.

Risk

Low. The dequant arithmetic and the converter fusion both look right and are covered against a blockwise reference including the shapes that motivated the patch; the remaining finding is about which checkpoints the gate lets in, not about what it does with them, and it fails loud rather than quietly. All three items are local fixes.

🤖 Generated with Claude Code

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 9902c0d

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/claude review

" Set MODELOPT_EP_DEGREE to the world size if it divides the expert count, or "
"MODELOPT_FSDP_SIZE=1 if the dense part fits one GPU."
)
elif ep_size > 1 and _ckpt_quant_method(hf_config) == "fp8":

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] The two unshardable reasons are mutually exclusive by elif, but both can hold at once — and the branch that wins then hands out advice the other one refuses.

A block-fp8 MoE on 8 ranks with MODELOPT_EP_DEGREE=2 and dense="fsdp" takes the first branch (world_size // ep_size == 4 > 1) and gets its remedy: "Set MODELOPT_EP_DEGREE to the world size if it divides the expert count, or MODELOPT_FSDP_SIZE=1 …". Following the first half (MODELOPT_EP_DEGREE=8) collapses edp to 1, which is exactly what makes this new elif fire — so the next attempt is refused again, now for the block-fp8 reason, and only the second attempt surfaces the remedy (MODELOPT_FSDP_SIZE=1) that was going to be the answer all along. Two refusals and two edits to get one piece of advice.

Cheapest fix is to let the fp8 reason append rather than compete, so a checkpoint that hits both is told both at once:

    unshardable, remedy = None, ""
    if ep_size > 1 and world_size // ep_size > 1:
        unshardable = (
            f"within each of its {world_size // ep_size} expert-data-parallel replicas, a layout "
            "the export cannot write yet"
        )
        remedy = (
            " Set MODELOPT_EP_DEGREE to the world size if it divides the expert count, or "
            "MODELOPT_FSDP_SIZE=1 if the dense part fits one GPU."
        )
    if ep_size > 1 and _ckpt_quant_method(hf_config) == "fp8":
        unshardable = (
            "from a block-fp8 checkpoint: Distributed Checkpoint reads it as stored, with no "
            "dequantization"
        )
        remedy = (
            " Set MODELOPT_FSDP_SIZE=1 to replicate it, which dequantizes it as it loads, or start "
            "from an unquantized copy of the model."
        )

(The fp8 reason overwriting the edp one is right: it is the one that survives every MODELOPT_EP_DEGREE the user can pick.)

# mxfp4 is for experts stored in the standard layout: GPT-OSS's are transposed, which the load refuses.
_EP_SOURCE_FORMATS = frozenset({None, "mxfp4"})
# fp8 is block FP8; _ckpt_quant_method calls per-tensor FP8 fp8-per-tensor, which the load refuses.
_EP_SOURCE_FORMATS = frozenset({None, "fp8", "mxfp4"})

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] The refusal an AutoFP8 user now gets reads as a contradiction. _EP_SOURCE_FORMATS is rendered straight into the message at :2195-:2199, so a per-tensor FP8 checkpoint produces:

The expert-parallel load cannot dequantize a 'fp8-per-tensor' checkpoint: it loads unquantized checkpoints and dequantizes fp8, mxfp4. Start from an unquantized copy of the model.

Their config.json says "quant_method": "fp8", and the message says it dequantizes fp8 — yet it refused. The distinction (weight_block_size present or not) is nowhere in the text, so the natural next step is to assume a bug and retry, not to go find a bf16 copy. The invented "fp8-per-tensor" label is doing the discriminating work but reads like a format name the user should recognize, and they won't find it in any checkpoint.

Since the set is both the gate and the message, a small display mapping keeps the gate exact and the message honest — e.g. render fp8 as block fp8 in dequantized, and say in the refusal that a fp8 checkpoint without weight_block_size is per-tensor FP8, which this load does not dequantize. One sentence, and the user knows which of their two FP8 checkpoints to use.

# weight_scale_inv. Mirror get_model's mxfp4 dequant-on-load, using FineGrainedFP8Config(dequantize=True).
# (compressed-tensors pack-quantized experts, e.g. Kimi, are handled separately by the on-the-fly
# decompressor plugin; here we only cover the native fp8 case.)
if quant_method == "fp8":

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] ModelOpt already has a block-FP8 path, and this comment doesn't say why it isn't the one being used here — while the repo's own PTQ skill tells readers the opposite of what this line does.

modelopt/torch/quantization/plugins/huggingface.py registers _QuantFP8Linear for FP8Linear (:1393) and _QuantFP8Experts for FP8Experts (:1390), both doing lazy per-use blockwise dequant — and _QuantFP8Experts.get_expert_weight (:1096-:1104) is the same tile-and-crop arithmetic this patch adds, partial-last-block crop included:

scales = scales.repeat_interleave(rows, 0).repeat_interleave(cols, 1)
return (weight.float() * scales[: weight.shape[0], : weight.shape[1]]).to(dtype)

Meanwhile plugins/modelopt/skills/ptq/references/unsupported-models.md:247 says, in bold: "Do NOT use FineGrainedFP8Config(dequantize=True) — it expands the entire model to BF16 upfront, wasting ~2x GPU memory. The plugin approach is both more memory-efficient and simpler." An agent loading that skill and then reading this code gets contradictory instructions, and a human reviewer has to re-derive the answer.

I believe the answer is that the lazy path genuinely cannot serve this load — _QuantFP8Experts is _eager_only and keys off transformers' FP8Experts module, whereas the EP path needs materialized bf16 fused gate_up_proj/down_proj params for enable_expert_parallel's grouped-GEMM sharding and the torch_a2a _grouped_linear, which no FP8 expert module provides. If that's right, say it in the comment (one clause: "not _QuantFP8Linear/_QuantFP8Experts: the grouped-GEMM EP shard needs materialized bf16 fused expert params, which the lazy FP8 modules do not provide") and consider updating the skill reference to scope its "do not" to the non-EP path. If a shared helper for the tile-and-crop is cheap to extract, that removes the third copy of the arithmetic too — but the comment is the part that costs nothing and saves the next reader the trace.


_FineGrainedFP8HfQuantizer.update_weight_conversions = _uwc_fixed
_FineGrainedFP8HfQuantizer._modelopt_uwc_patched = True
except Exception as e:

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] except Exception turns every failure in this block into the same claim about the cause, and one of those failures isn't that cause.

The block's own guard — hasattr(_FineGrainedFP8HfQuantizer, "update_weight_conversions"), with the comment "A transformers without the hook prepends no dequant to the expert converters: nothing to fuse" — sits after the three imports it is meant to cover. On a release that has no transformers.core_model_loading at all (the EP floor here is require_transformers("5.6"), and the new test module skips on 5.5 for lacking Fp8Dequantize, so there is a window between the floor and where the conversion-op loader landed), the import raises first and the user is told:

block-FP8 load: could not fuse transformers' expert dequant into its merge (No module named 'transformers.core_model_loading').

On such a release nothing prepends a dequant and nothing would be row-sliced — the stated failure mode cannot occur there. The refusal itself is defensible (that transformers almost certainly can't do this load anyway), but it points at the wrong thing, and test_an_expert_converter_it_cannot_fuse_stops_the_load pins exactly this path by removing core_model_loading from sys.modules, so the test's name encodes the same mis-attribution.

Two options, either fine: name the version floor in the message ("this transformers has no conversion-op loader; block-fp8 expert-parallel loads need >= X"), or hoist the quantizer import and the hasattr check ahead of the core_model_loading imports so "nothing to fuse" is decided before anything that can fail for an unrelated reason. Also worth narrowing except Exception to (ImportError, AttributeError) — as written, a genuine bug introduced later in _DequantThenMerge's class body would be reported as a transformers-compatibility problem.

@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 — block-FP8 dequant on the expert-parallel load

Scope: full review (the trigger comment carried no scoping instructions). All 5 changed files reviewed against shengliangx/hfep-6b-ep-dense-sharding: modelopt/torch/utils/plugins/model_load_utils.py (+260/-7) plus the four test files. For composition I also read _decide_distribution whole, _load_sharded's guards at :437-:480, the dtype resolution at :2212, and — new this round — _QuantFP8Linear / _QuantFP8Experts in modelopt/torch/quantization/plugins/huggingface.py, since they are the project's other block-FP8 mechanism.

Findings: CRITICAL: 0, IMPORTANT: 0, SUGGESTION: 4

Resolved since the last round

  • Per-tensor FP8 no longer slips into the block-FP8 path. _ckpt_quant_method now reads a quant_method: fp8 without a weight_block_size as "fp8-per-tensor", which _EP_SOURCE_FORMATS refuses before _resolve_checkpoint_dir, so an AutoFP8 checkpoint fails loud instead of dying inside transformers' block loader with weights scaled by 1. The discrimination is right for the formats in the wild: DeepSeek-V3 and the block-FP8 Qwen checkpoints carry weight_block_size; AutoFP8's per-tensor and per-channel configs do not, and an explicit "weight_block_size": null falls on the refusing side too. The 448-vs-0.079 table in the description is the right evidence for why this had to be a refusal rather than a warning.
  • The dense layout is now decided and enforced consistently. _decide_distribution replicates a block-fp8 dense part instead of leaving dense="auto" free to pick fsdp, refuses an explicit fsdp naming MODELOPT_FSDP_SIZE=1, and load_and_prepare_ep repeats the refusal at :2188 for a plan handed to it directly. The guard is correctly fp8-only and not extended to mxfp4: gpt-oss's quantized tensors are all expert tensors, which from_pretrained loads over the ep submesh, so only block FP8 — which covers the dense linears as well — breaks under a DCP-sharded dense load.
  • The ep_size=1 corner the last round raised is now pinned by a test, and it fails loud: _load_sharded refuses any quantization_config at :477 before a weight is read.

Verification I ran on the arithmetic (no finding, recorded so it isn't re-derived)

  • s[..., :whole, None] really does slice the scale columns — None adds an axis without consuming one, so (…, rows, scale_cols) becomes (…, rows, whole, 1) against an unflattened (…, rows, whole, block_n). q[..., :whole * block_n] is basic slicing, and unflatten(-1, (whole, block_n)) on strides (…, cols, 1) is expressible as a view, so the mul_ is genuinely in place on q's own copy. whole == 0 (narrower than one block) and scale_cols - whole ∈ {0, 1} both broadcast correctly, and the row tile is long enough under both _blk branches.
  • Double-wrapping on the >= 5.10.1 path is harmless in both directions, not just one: the outer wrapper's padded tensor is fp32, so the inner falls through on quantized.dtype not in fp8_dtypes; and on a non-partial weight the outer bypasses and the inner independently re-derives the same bypass. The FP4 dtypes (int8 / float4_e2m1fn_x2) are excluded from fp8_dtypes, so upstream's unpack and ue8m0 decoding are never routed through the pad.
  • dtype = getattr(hf_config, "dtype", None) or torch.bfloat16 is always concrete at the call site, so _FP8_DEQUANT_DTYPE can never be left at a previous load's value by a None argument.
  • The patch is called on every rank, unconditionally, and can only raise identically on all of them — the master-only print is the sole rank-dependent statement, and it issues no collective. No divergence.

Suggestions (non-blocking, 4 inline)

  • The two unshardable reasons are elif-exclusive but can both hold; the edp branch then wins and its remedy (MODELOPT_EP_DEGREE=<world>) leads straight into the fp8 refusal, so the user needs two failed attempts to learn MODELOPT_FSDP_SIZE=1. Letting the fp8 reason overwrite rather than compete is a three-character change.
  • The per-tensor refusal reads as a contradiction to the person receiving it: "cannot dequantize a 'fp8-per-tensor' checkpoint: … dequantizes fp8, mxfp4", when their config.json says fp8. Rendering fp8 as block fp8 in the message, and naming weight_block_size as the discriminator, costs one sentence.
  • The comment at :2266 doesn't say why _QuantFP8Linear / _QuantFP8Experts — ModelOpt's existing lazy block-FP8 dequant, whose get_expert_weight is this same tile-and-crop including the partial-block crop — aren't the mechanism here, while plugins/modelopt/skills/ptq/references/unsupported-models.md:247 tells readers in bold not to use FineGrainedFP8Config(dequantize=True). The reason looks sound (the grouped-GEMM EP shard needs materialized bf16 fused params); it just isn't written down, so the code and the skill read as contradicting each other.
  • The fuse's except Exception reports a missing transformers.core_model_loading as "could not fuse transformers' expert dequant into its merge" — a cause that cannot exist on a release without the conversion-op loader, which the hasattr guard was meant to cover but sits after the imports.

What reads well

  • _ckpt_quant_method doing the per-tensor discrimination in the one place both call sites already go through, rather than adding a second predicate, and keeping the refusal ahead of _resolve_checkpoint_dir so a refused load downloads nothing.
  • The fp8-only dense guard, reasoned from which tensors each format quantizes rather than from the format name — that is why extending it to mxfp4 would have been wrong, and the asymmetry is the kind of thing that usually gets "fixed" into a bug later.
  • test_undoing_the_patch_restores_stock_transformers diffing vars(cls) to assert the patch rebinds only its three targets, which is what makes the refusal test meaningful; and the rebind following the fuse, so a refused patch leaves transformers byte-identical.
  • The test table carrying its own reasoning (grid-divides-the-rows: 9 rows of 4-row blocks give a 3-row grid that also divides 9) — the cases that would pass under the naive rule are the ones written down.
  • Both < 5.10.1 refusal paths fail loud on exactly the states 5.9's tensor_idx bug produced, instead of loading unscaled weights.

Risk

Low. The arithmetic in both version branches holds up against a re-derivation and is covered against a blockwise reference on the shapes that motivated the patch; the dense-layout decision and its two enforcement points agree; and the new refusal closes the one admission gap left from the last round. All four remaining items are message clarity, comment provenance, and one exception-handling narrowing — none of them change what the code computes.

🤖 Generated with Claude Code

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 37db5f8

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 12aee3f

A native block-FP8 checkpoint (e.g. DeepSeek-V3) is dequantized to bf16
while it loads, through transformers' FineGrainedFP8Config, since
quantizers attach to bf16 weights. Its dequant is corrected on
transformers < 5.10.1 for weights whose last 128x128 block is partial, and
made to emit the model's dtype.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…out the session

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The patch and its module flags sit in model_load_utils with the
expert-parallel loader that applies them, so the test drives and resets
them there.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The patch fuses the Fp8Dequantize that transformers prepends to the
routed-expert converter into its MergeModulelist, so the loader still
gives each expert its tensor_idx. Only the install was tested. The new
test runs the patched update_weight_conversions on an expert converter
and the resulting chain on per-expert block-FP8 weights. It fails if the
converter no longer leads with a MergeModulelist, or if the fused op
skips the dequant or the merge. transformers 5.18's chain passes an
output_dtype the patched dequant does not take, so the chain runs that
version's own dequant; the patched arithmetic has its own tests.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
- The two block-size cases, a grid finer than the configured block and
  no block configured, become rows of the blockwise-reference table, which
  gains configured and grid columns.
- The dtype test keeps its unrequested (bf16) and requested (fp16) cases;
  requesting bf16 or fp32 took the same branch.
- The version test keeps the 5.10.1 boundary; 5.17.0 took the same branch.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The pre-5.10.1 dequant patch emitted a non-empty weight unscaled when its scale
grid was empty. That only happens when a weight and its scales were sliced
apart, as transformers 5.9's expert tensor_idx bug did on expert-parallel loads,
so it loaded wrong weights without a word; upstream's own dequant fails on
such a grid. It also fell back to the configured block for a grid neither that
block nor an even split gives, scaling rows wrongly. Raise in both cases; an
empty weight still passes through its empty grid.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
From 5.10.1 the patch kept transformers' own dequant untouched, on the
assumption that no shipping model has a partial last block there.
DeepSeek-V3, V3.1 and Kimi-K2-Instruct do: kv_a_proj_with_mqa has 576
rows under a 5-row grid of 128-row blocks, and Fp8Dequantize still
raises "not divisible by scale grid" for it, so their block-FP8 loads
failed. The stock dequant is kept, for its ue8m0 and FP4 decoding and
output_dtype, and wrapped: an FP8 weight whose grid is the configured
block's ceil grid is padded to whole blocks for it and cropped back.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
When fusing transformers' prepended expert dequant into its merge
failed, the patch printed a warning and went on. Every routed expert
then loaded sliced apart from its scales. The failure now raises. A
transformers without the update_weight_conversions hook prepends no
dequant, so there is nothing to fuse and the patch skips it.

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

The load dequantizes native block-FP8 checkpoints as it loads, so they
join the formats it accepts.

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

The hub-ID test now loads an unquantized, a block-FP8 and an MXFP4
config: each must reach the checkpoint's resolution, past the format
check, so dropping either format from the accepted set fails it.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
From transformers 5.10.1 the expert-parallel load wraps transformers' block-FP8 dequant, which derives
the block size from the scale grid, and padded a weight to whole blocks only when it did not divide
into that grid. A partial last block can still divide evenly: 448 rows under a 4-row grid of
128-row blocks go to the dequant as blocks of 112, and every row group after the first gets the
wrong scale, silently. The wrapper now pads whenever the weight is not block-aligned, which is a
no-op for whole blocks.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The pre-5.10.1 dequant multiplies its own fp32 copy of the weight by the tiled scales in place,
so a weight holds two full-size fp32 temporaries instead of three while it loads. The dequant is
rebound after the expert converter's fuse, so a refused patch leaves transformers as it was.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The rule that a block-FP8 checkpoint's dense part is not sharded lands here, with the load that
makes such a checkpoint loadable, rather than in [10/21], where it preempted the format refusal.
Left to decide, the plan replicates that dense part and says why; asked to shard it, the decision
refuses, naming MODELOPT_FSDP_SIZE=1, and the loader refuses a plan that shards it.

The pre-5.10.1 dequant scales its fp32 copy of a weight in place, through a view of its whole
column blocks and then the partial last one, against scales tiled over the rows only. It builds no
full-size fp32 scale tile: an (18432, 7168) weight peaks at 6 bytes per element instead of 8. A
weight narrower than one block joins the reference table.

The expert converter's fuse no longer skips a merge it already fused, a layout transformers'
update_weight_conversions never produces (it builds fresh converters on every call); fusing it
again would dequantize once all the same. The load's log names the dtype the dequant emits.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
quant_method "fp8" is not block FP8 alone: per-tensor FP8 checkpoints (AutoFP8's: a scalar
weight_scale per weight, no weight_block_size) name the same method. The expert-parallel load
admitted them, and transformers' dequant reads them as 128x128 blocks with no weight_scale_inv, so
their weights load unscaled without an error (on transformers 5.18 a tiny Llama loaded that way
has max |w| 448 where the original has 0.079). _ckpt_quant_method now calls an fp8 checkpoint
without a weight_block_size "fp8-per-tensor", which the load refuses, before anything loads, as a
format it cannot dequantize; the decision's block-FP8 rule no longer applies to it either.

The decision, the loader's refusal and its docstring give the reason a block-FP8 dense part cannot
be sharded as Distributed Checkpoint reading it as stored, with no dequantization: its scales'
format was never the blocker (DeepSeek-V3's are fp32). A comment says where the dequant's block size
comes from: transformers keeps the checkpoint's quantization_config and takes only the loading
attributes from the one the load passes.

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

Copy link
Copy Markdown
Collaborator Author

/ok to test 3c17f8a

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.

2 participants