Repository navigation
[OMNIML-6071] Calibrate a distributed model on per-rank data - #2740
Open
shengliangxu wants to merge 60 commits into
Open
shengliangxu wants to merge 60 commits into
shengliangxu wants to merge 60 commits into
Conversation
|
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. |
Contributor
shengliangxu
force-pushed
the
shengliangx/hfep-4-meta-dcp-load
branch
from
October 12, 2026 02:24
1b34e71 to
c99d63a
Compare
shengliangxu
force-pushed
the
shengliangx/hfep-5-distributed-calibration
branch
from
October 12, 2026 02:24
7f91f94 to
963e78a
Compare
Contributor
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #2740 +/- ##
==========================================
+ Coverage 80.29% 80.78% +0.48%
==========================================
Files 675 663 -12
Lines 77278 74300 -2978
==========================================
- Hits 62054 60025 -2029
+ Misses 15224 14275 -949
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Collaborator
Author
|
/ok to test 963e78a |
shengliangxu
force-pushed
the
shengliangx/hfep-4-meta-dcp-load
branch
from
October 12, 2026 08:39
c99d63a to
d0d9b5e
Compare
shengliangxu
force-pushed
the
shengliangx/hfep-5-distributed-calibration
branch
from
October 12, 2026 08:39
963e78a to
935bd7f
Compare
Collaborator
Author
|
/ok to test 935bd7f |
shengliangxu
force-pushed
the
shengliangx/hfep-5-distributed-calibration
branch
from
October 13, 2026 00:53
935bd7f to
c3ccb86
Compare
shengliangxu
force-pushed
the
shengliangx/hfep-4-meta-dcp-load
branch
from
October 13, 2026 00:53
d0d9b5e to
64f8588
Compare
shengliangxu
force-pushed
the
shengliangx/hfep-5-distributed-calibration
branch
from
October 13, 2026 01:32
c3ccb86 to
96159d7
Compare
shengliangxu
force-pushed
the
shengliangx/hfep-4-meta-dcp-load
branch
from
October 13, 2026 01:32
64f8588 to
7da05a2
Compare
Collaborator
Author
|
/ok to test 96159d7 |
shengliangxu
force-pushed
the
shengliangx/hfep-5-distributed-calibration
branch
from
October 13, 2026 19:38
96159d7 to
540716e
Compare
shengliangxu
force-pushed
the
shengliangx/hfep-4-meta-dcp-load
branch
from
October 13, 2026 19:38
7da05a2 to
00e5f72
Compare
Collaborator
Author
|
/ok to test 540716e |
shengliangxu
force-pushed
the
shengliangx/hfep-4-meta-dcp-load
branch
from
October 14, 2026 00:14
00e5f72 to
42a2de6
Compare
shengliangxu
force-pushed
the
shengliangx/hfep-5-distributed-calibration
branch
from
October 14, 2026 00:14
540716e to
177c692
Compare
A stream that splits itself across the loader's workers by get_worker_info(), as a Hugging Face streaming dataset does, was split again by rank and silently lost samples: the shards stayed disjoint and the step counts equal, so nothing failed. shard_dataloader now refuses a streaming loader with workers, and the rank's shard no longer splits its groups across workers it cannot have. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
shard_dataloader refuses a streaming loader with workers, so asking the stream to be the same in every worker, and explaining that torch seeds each worker differently, described a case that cannot arise. _StridedIterableShard's docstring carried the same clause. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
shard_dataloader let any RandomSampler through, but one drawing fewer samples than the dataset (num_samples) or with replacement picks which and how many samples the loader reads, as a subset does. The rank's DistributedSampler then replaced it over the whole dataset, so calibration silently ran on all of it instead of the few asked for. Such a sampler is now refused like the other pickers; a sampler that shuffles the whole dataset is still sharded as the rank's unshuffled share. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The refusal reads the algorithm's options as the caller spelled them, which holds while every registered default syncs over the ranks; the comment names those defaults, so one that stops syncing is known to need refusing here. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The test created its own gloo group and destroyed it at once when the job ran no collective on it, as the no-plan case does. gloo's new_group can return on one rank while a peer is still connecting, so the peer's new_group failed with "Connection closed by peer" (seen once in CI). bounded_collectives makes the same bounded group and passes a barrier on it first. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Nothing defers them: modelopt.torch.utils.logging, which dataset_utils imports, already imports modelopt.torch.utils.distributed at its top. The hf_ptq argument test's new imports move to the top too. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The rank-local refusal read the algorithm dict as the caller spelled it, so
{"method": "max", "distributed_sync": "false"}, which MaxCalibConfig coerces
to False, calibrated without the sync. Each mode's options are now validated by
its config class first, nested scale algorithms included, so coerced values and
defaults count as calibration reads them. The test spells the sync off as
"false", and gives nvfp4_act_headroom its local_hessian weight-scale algorithm
in the form its schema accepts.
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…arallel size A data-parallel group of the plan's edp ranks passed the expert-parallel check even when it was the experts' own expert-parallel group, whose ranks hold other experts: at EP 2 over four ranks, max's and GPTQ's syncs would then merge different experts' scales. A routed expert's ParallelState now passes only with an expert-parallel group of ep_size ranks and a data-parallel group of edp ranks that meets it at this rank alone. The two-rank pool cannot build such groups, so the new test answers four ranks' process-group queries. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…t distributed awq_clip's static-block branch already all-reduced each candidate clip's loss over the module's data-parallel group; its per-tensor branch, which NVFP4 takes, kept each rank's own, so the ranks' copies of a weight took different clips. Both branches now average through one helper, issued on every rank in the same order, and awq_clip is no longer refused on a model distributed over several ranks. This also makes Megatron data-parallel replicas agree on the per-tensor clip. Tests: a two-rank CPU test of the static-block branch, and a two-GPU NVFP4 test of the per-tensor branch, each with ranks whose own data would pick different clips. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The schema-validated refusal read distributed_sync with "is False", but max and
MSE accept None too, which skips their sync as False does, so
{"method": "max", "distributed_sync": None} calibrated the ranks apart. Any
falsy value is now refused, nested scale algorithms included. The refusal test
gains an MSE weight-scale algorithm with distributed_sync None.
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
… columns The previous check passed any expert-parallel group of ep_size ranks and data-parallel group of edp ranks meeting it only at this rank, so diagonal data-parallel groups ([0, 3] and [1, 2] at EP 2 over four ranks, where ranks 0 and 2 hold the same shard) still merged different experts' scales. A routed expert's groups must now be this rank's row of the (edp, ep) mesh the expert-parallel load lays the ranks out in, as its expert-parallel group in row order, and its column as its data-parallel group. The four-rank test adds the diagonal data-parallel group and an expert-parallel group in another order. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
… Fixes It is not backward breaking and not specific to hf_ptq: it also changes the amax values that data-parallel runs (Megatron included) export. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…the forwards The data-parallel mean ran inside each forward, in both branches (the static-block one on main too), so a module issued its all-reduces once per forward it ran. An MoE expert's forward count is each rank's own, so ranks that ran it a different number of times paired mismatched collectives and hung. Each rank now accumulates its own losses, and after the forward loop every module's candidate losses and token count are reduced once over its data-parallel group, in module order on every rank: the same numbers, as the steps are equal, with a collective count the model fixes. That also replaces up to one all-reduce per candidate, step and output block with one per module. The two-rank test now runs an expert a different number of times on each rank; it fails without the change, on the bounded group's timeout. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…rmost The row and column check took the ep axis as the mesh's innermost, as the expert-parallel load builds it today, and nothing ties the two together. It now accepts this rank's row and column of the mesh with either axis innermost: in both, the data-parallel group holds this rank's shard. Any other grouping is still refused. The four-rank test adds a mesh with ep outermost. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
An MoE expert a rank routes no tokens to can still be called, on an empty input. The per-tensor search then added a NaN loss (a mean over no outputs) for every candidate, which the post-loop sum spread to the replicas that had tokens, leaving them a clip of 0, and the static-block search failed to reshape the empty input. The search now returns at once for an empty input, leaving the losses and token count as they were, and the post-loop reduction still runs on every rank. Both distributed tests now run an expert on no tokens on one rank and check its clips are positive and those a single process picks from the other ranks' tokens: the CPU one (static-block branch) fails without the change on the reshape, the GPU one (NVFP4, per-tensor) would on a clip of 0. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…eads it hf_ptq --use_fsdp2 recorded the FSDP2 plan after its load, so that calibration sized its batch collectively and refused rank-local calibrations there. That path still retries a batch after a rank-local out-of-memory error, and the fix for that belongs where hf_ptq moves onto the run's abort path. The recording now lands with the no-gather export, the first change whose export needs the plan on that path, together with that fix. The README notes and the changelog entry on the refusal go with it; the AWQ-Clip ones stay, as its sync reads no plan. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…t, on GPUs A two-or-more-GPU test of the planned path end to end: a model under fully_shard records its plan, get_max_batch_size measures each rank's GPU in one forward (rank 0 holding a fortieth of its free memory per sample, every other rank a sixth) and every rank takes the smallest size, not its own; then each rank calibrates its shard with max through distributed_forward_loop. Every rank runs the same three steps and ends with the same scales. The batch size's reduction and the test's gathers give up in seconds on a bounded group. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
A two-or-more-GPU test of the expert-parallel calibration lifecycle on stock transformers: a tiny Qwen3-MoE loaded with its experts sharded over every rank by transformers' own expert-parallel load on the torch_a2a styles, seeded with the experts' groups, quantized with FP8 through distributed_forward_loop, each rank on its own batch. Every expert's weight scales and every activation scale match the whole model calibrated on every rank's batch. On transformers >= 5.16 quantizer insertion drops the transport's forward wrappers, which the loop re-plants: the test fails without the re-plant, and without the conversion keeping torch_a2a (#3074). The scales' syncs and the test's gathers give up in seconds on bounded groups. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Accepting the (edp, ep) mesh with either axis innermost left the check open when edp equals ep_size: there the two layouts' (row, column) pairs are each other's transpose, so experts whose expert-parallel and data-parallel groups were swapped passed, and max's and GPTQ's syncs merged different experts' scales. The check is exact again for the layout the expert-parallel load builds, ep innermost, and refuses any other. The four-rank test's transposed groups, which the either-way check accepted, are now refused. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…y weight gather The token count now rides in the fp32 losses' buffer, exact for any calibration set, so each module issues one all-reduce instead of two. And every module's reduction runs in its own pass ahead of the postprocess pass, whose weight access an FSDP2 model gathers for, instead of alternating with those gathers. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The data-parallel sync stacked every candidate clip's loss into a new tensor, packed the token count after it and divided out of place, so while it ran it held up to three copies of the losses beyond the module's own. Each candidate's loss is now a view of one buffer, which the sync reduces and averages in place, and the token count goes in its own int64 all-reduce, exact for any count. Every module's reductions still run ahead of the weight gathers. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The refusal advised calibrating with max, MSE, GPTQ, SmoothQuant or awq_clip "with distributed_sync on", but only max and MSE have that option. And local_hessian, refused although it has the option, says why where it is listed: its distributed_sync syncs only the max amax it starts from. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…uantizing The calibration guard runs before quantization converts anything, so on an expert-parallel plan it reads the routed experts' ParallelState from the caller, as the expert-parallel load seeds it, and checks their quantized modules again before they calibrate. The docstring now says so instead of implying that only the converted modules are checked. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
_StridedIterableShard takes one sample in world_size from the stream each rank builds, so every rank still reads and decodes all of them. Its docstring now says so, and that a map-style dataset, or a source that shards itself, saves that work for a stream costly to decode. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Main now refuses AWQ-Lite and AWQ-Full on an FSDP2 model sharded over several ranks, and its README note on that sits right above this PR's note on nvfp4_awq_clip. The two notes are now separate paragraphs, and this PR's says why nvfp4_awq_clip is not refused: AWQ-Clip folds no smoothing scale into the weights, and its clip search is averaged over the ranks. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
AutoQuantize calibrates and scores through forward_step on the model it has converted, and conversion drops the expert-parallel forwards that distributed_forward_loop restores. On an expert-parallel model the first of those forwards would fail with the experts forward's missing process_group error, after the model was converted. auto_quantize now refuses a model whose recorded plan shards its experts, before it changes anything, as quantize and calibrate refuse what they cannot calibrate. A two-rank test with seeded experts and an INT8 candidate checks the module types and state are left as they were. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
shard_dataloader replaces a loader's sampler with the rank's DistributedSampler over the loader's dataset. It checked a RandomSampler's sample count against the sampler's own source, so a SequentialSampler, RandomSampler or DistributedSampler over a shorter source than the dataset passed, and the shard then calibrated on samples the loader never read. The coverage is now compared with the loader's dataset; a sampler that orders or shuffles the whole dataset is still replaced as before. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Only the three known samplers reach len(source), and each has a data_source or a dataset; the empty default only keeps the expression typed. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
_reinstall_a2a_forwards planted the torch_a2a wrapper on any planned module whose instance forward was missing or a bound method. The clobber it repairs always leaves a bound method: a module with no instance forward is one the load never wrapped, and planting there would add the style's transforms to a module that ran without them. It now plants only over a bound method, as its docstring says. The pre-hook test now starts from rebound forwards, and a new test checks that a module the load did not wrap is left alone. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Collaborator
Author
|
/ok to test dea9b79 |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
The expert-parallel PTQ series is now a tree instead of one stack (re-cut 2026-10-27, from main 1521053), so PRs that do not depend on each other can be reviewed and merged in parallel. This PR was [8/21]. Rebased onto main 7c3c868 later the same day, after #2739 merged, and onto main f03651e that night.
Re-cut: this PR's diff is unchanged; only its base moved. Review fixes since are listed at the end of Testing.
Series layout
mainWhat does this PR do?
Type of change: New feature
The pieces a calibration loop needs once the model is spread across ranks.
shard_dataloadergives each rank its own shard of the data.DistributedSamplerwithdrop_lastoff, so every rank runs the same number of steps; a rank that exits early hangs the others on the per-step collectives.IterableDatasetgets a strided copy of the stream, which must yield the same samples in the same order on every rank (a stream that shuffles needs a fixed seed of its own). A streaming loader with worker processes is refused: a dataset that splits itself across workers, as a Hugging Face streaming dataset does, would lose samples silently. It is not main'svlm_dataset_utils._ShardedIterable, which pads each rank to a count its loader knows: this wraps any loader's stream, which may have no length.distributed_forward_loopbuilds theforward_loopthatmtq.quantizecalls. It moves each batch to thedeviceit is given and runs the full distributed forward, so the expert-parallel collectives fire inside it, with KV caching off as on the single-process path. Each batch is a mapping of the forward's keyword arguments; anything else is refused with aTypeErrorat the first batch.ep_meshits experts are sharded over, and the loop re-plants thetorch_a2aforward wrappers that quantizer insertion clobbers on transformers >= 5.16. A module whose forward is still the wrapper planted there is skipped, so passes do not stack wrappers; one whose forward was rebound since (another conversion, an export) is planted again. The planted wrappers stay on the model, so its later forwards (a preview's, an export's) work once the loop has run.mtq.auto_quantizecalls itsforward_stepdirectly and would plant nothing, so it refuses a model whose recorded plan shards its experts, before converting anything. Any other model passes none. A mesh missing for an expert-parallel model, or given for another, raises.--batch_size 0sizes calibration withget_max_batch_sizeon each rank: it measured every visible GPU, and on an out-of-memory error halved the batch and probed again, a different number of collective-bearing forwards per rank. For a model whose recorded plan spreads it over several ranks it is now collective: each rank measures the GPU it computes on (torch.cuda.current_device()) from one forward of the single-sample input, without probing the size it arrives at (a rank running out of memory partway through a probe's forward would leave its peers waiting in that forward's collectives), and every rank takes the smallest size any rank allows, reduced withall_reduce(..., min)([OMNIML-6052] Add the distribution plan, a run-scoped process group, and collectives that fail together #2736). A single process, and a model with no plan recorded (a Megatron model, say), are unchanged.maxall-reduces its amax, GPTQ merges its Hessians across data-parallel replicas, and MSE and SmoothQuant work from max's synced amax. AWQ-Lite does not: it averages its activation scales over data parallelism but picks each layer's scale from its own rank's search, and a 2-rank probe showed the replicas' weights andpre_quant_scalediverging.awq_clipnow averages each candidate clip's loss over data parallelism before the ranks pick one, in both branches: each rank accumulates its own losses, and after the forward loop every module's losses and token count are all-reduced once, in module order, so the collective count does not depend on how many forwards a rank ran a module (an MoE expert's is each rank's own). Its static-block branch averaged per forward before, and its per-tensor branch, which NVFP4 takes, not at all; this also makes Megatron data-parallel replicas agree on the per-tensor clip.local_hessianandimatrixshare nothing either, and neither do max and MSE withdistributed_syncoff.mtq.quantize,mtq.calibrateandmtq.auto_quantizerefuse these (awq_lite,awq_full,svdquant, which runsawq_lite,local_hessian,imatrix, and any config that turnsdistributed_syncoff, withFalseorNone, also as another algorithm'sweight_scale_algorithmorscale_algorithm) on a model whose recorded plan spans several ranks, before converting anything (auto_quantizein its format check). A model with no plan recorded, such as a Megatron model, is unchanged. On an expert-parallel plan, any calibration is refused unless every module holding routed experts carries, as its expert-parallel group, the rank's row of the(edp, ep)mesh the expert-parallel load lays the ranks out in, ep innermost (in row order, which picks each rank's expert shard), and, as its data-parallel group, its column, the plan'sedpranks holding the same experts: each rank's per-expert quantizers hold its own experts, and max's and GPTQ's data-parallel sync would otherwise merge different experts' scales. The modules holding routed experts are those carrying an expert-parallel group and, once quantization has converted them, the quantized modules under a container namedexperts, as transformers' expert-parallel plans name it; unseeded experts beside a seeded router are refused once converted, before they calibrate. [OMNIML-6060] [3/3] Refuse an expert-parallel load that would calibrate the experts wrong #2741's expert-parallel loader gives the routed experts those groups. A module with no routed experts and no expert-parallel group, such as a VLM's vision tower that hf_ptq quantizes on its own with every quantizer disabled, passes.--use_fsdp2path is not changed here. Its load (parallel_load_and_prepare_fsdp2) records no plan, so the batch sizing and the refusal above do not apply to it yet. Recording the plan there lands with [OMNIML-6083] Write a sharded model's HF checkpoint without gathering it #2752, the first PR whose export reads it on that path, together with stopping a planned run from retrying a batch after a rank-local out-of-memory error; [OMNIML-6085] [3/3] Run expert-parallel PTQ from hf_ptq.py #2754 then moves the load ontoload_pretrained, which records the plan itself.nvfp4_awq_clip's data-parallel clip (below) needs no plan, so it applies to--use_fsdp2runs now.shard_dataloaderrefuses a loader that picks its own samples (a custombatch_sampler, a sampler other than sequential, random or distributed, or one of those over another source than the loader's dataset, also inside a plainBatchSampleror with auto-batching off) instead of rebuilding it, and keeps a plainBatchSampler's batch size anddrop_lastin the rank's shard; a loader with auto-batching off (batch_size=None) shards one item per step, as it loads. With one rank,shard_dataloaderreturns the loader itself, its sampler and shuffling included.distributed_forward_loopruns its forwards without gradients.get_max_batch_sizereads the recorded plan, as the calibration guard does.distributed_forward_looprefuses anep_meshwhose size is not the plan'sep_size.Usage
Testing
New
tests/unit/torch/utils/test_distributed_calibration.py(42 cases), on CPU, in one process except where noted. It covers:BatchSampler's batch size anddrop_lastincluded;use_cacheoff and the allocator cache freed around it in a process that started CUDA, and not in one that did not; a batch that is not a mapping refused;ep_meshrefused when missing for an expert-parallel plan, given for another, or over the whole world for anep_size=2plan;quantize,calibrateandauto_quantize(before conversion), also when another algorithm runs it for its weight scales or max is told not to sync, with the model left as it was;distributed_forward_loopruns on real GPU ranks in [OMNIML-6061] Load the dense part sharded, and size the load warmup to each rank #2742's expert-parallel calibration test, which checks each rank's per-expert scales against the whole model; this PR's own tests run it in one process.tests/unit/torch/utils/test_dataset_utils.py, on two real gloo ranks (cpu_dist_workers) with only the CUDA memory queries faked per rank: each rank measures only its own GPU, probes once, and both take the smaller batch size. A rank keeping its own size, measuring every GPU, or skipping the reduction fails it, the last after the group's 10 s timeout rather than by hanging; main's single-process retry test still passes. The two-rank test also runs a model with no plan recorded, which each rank measures on every visible GPU, as a single process does, without a refusal.Mutation-checked: deciding the reduction or the mesh other than by the plan fails a test (5 mutants), and so does dropping either refusal call, the nested-algorithm check, the
distributed_synccheck, hf_ptq's plan recording, the expert-parallel group refusal, or theep_meshsize check. Refusing every expert-parallel calibration, or reading the plan strictly inget_max_batch_size, fails a test too. Dropping the check on converted experts or the data-parallel size check, keeping a replant flag once set, or planting on every pass each fail a test. Rebuilding a lone rank's loader, refusing a module without routed experts, or passing any module with no expert-parallel group each fail a test too.The shard tests are one table over dataset kind and world size, the two replant tests are one, and cases a remaining test covers are dropped. Each drop is checked by mutation.
A subset sampler is refused, also under a plain
BatchSamplerand with auto-batching off, and so is a custom batch sampler; a lone rank keeps each such loader as it is. The sampler, batching, non-mapping-batch andauto_quantizecases each fail without their fix (mutation-checked).Locally on this revision, the stack rebased on main c6fa613: pre-commit over the whole PR against main, at every PR of the stack; and at the stack's tip the full
tests/uniton transformers 5.18 and 5.5 and on torch 2.8, and the hf_ptq example tests, each with the same failures as main in that environment. CI runs the full unit suite.GPU tests were not re-run on this revision.
Re-cut on 2026-10-27 (see the note at the top): pre-commit, mypy included, passes at this head over the whole change against main, and the series' PRs merged together give a tree byte-identical to the former stack's tip. This PR's own unit tests pass at this head on CPU (torch 2.14, transformers 5.18); CI runs the full unit suite.
Review fixes, 2026-10-27: the a2a forward wrappers are re-planted only where quantizer insertion rebound the forward, so a wrapper still in place is never wrapped twice (found by review: the double wrap ran a module's input transforms twice, which no real run has shown to fail; on 4 GPUs at transformers 5.16.1 the expert-parallel load tests pass with either behaviour, and the CPU calibration-loop test is what tells them apart); the install probe checks each style it calls. Each behaviour fix has a test that fails without it; pre-commit, mypy included, and this PR's own unit tests pass at this head.
Review fixes, 2026-10-27, round 2: the batch size is measured on the GPU the rank computes on (
torch.cuda.current_device()); a streaming loader with workers is refused; the docstring and the hf_ptq README say why the measurement does not probe. Each behaviour fix has a test that fails without it; pre-commit, mypy included, and this PR's own unit tests pass at this head.Rebased on 2026-10-27 onto main 7c3c868, after [OMNIML-6064] Load each rank's checkpoint shard straight into a meta-built model #2739 merged: pre-commit, mypy included, and this PR's own unit tests pass at this head, and the series' PRs merged together add no unit-test failure over main (tests/unit/torch utils, export, quantization and models, on transformers 5.18 and 5.5.4 and on torch 2.8). The hf_ptq README's FSDP2 AutoQuantize note follows main's KV-cache AutoQuantize support ([OMNIML-5987] [2/3] Support DP/FSDP2 KV AutoQuant and checkpoint resume #2664): weight and KV-cache AutoQuantize with
--use_fsdp2are experimental, and refused when a candidate format is one of those this PR refuses.Review fix, 2026-10-27 night:
shard_dataloaderrefuses aRandomSamplerthat does not draw the whole dataset once (othernum_samples, or with replacement), as it refuses other samplers.Review suggestion, 2026-10-27, last round: the comment over the sync guard names the algorithms' defaults it relies on.
Rebased on 2026-10-28, late night, onto [OMNIML-6052] Add the distribution plan, a run-scoped process group, and collectives that fail together #2736 at
cf4597dcbeb, on main1ca8bd1fc4d(after [OMNIML-6073] [1/3] Gather only FSDP2's own parameters in its weight-access window #3088 and [OMNIML-6073] [2/3] Quantize fused experts under grouped_mm; leave transposed layouts unquantized #2745 merged), without conflicts; this PR's own diff is unchanged. Its tests with [OMNIML-6052] Add the distribution plan, a run-scoped process group, and collectives that fail together #2736's and thehf_ptqargument tests pass on transformers 5.18 (220), and pre-commit passes against main.Rebased on 2026-10-28, late night, onto main
bdc45368d10after [OMNIML-6052] Add the distribution plan, a run-scoped process group, and collectives that fail together #2736 merged; [OMNIML-6052] Add the distribution plan, a run-scoped process group, and collectives that fail together #2736's squash has the tree of its last head, so this PR's tree is unchanged.Review fixes, 2026-10-29: the two-rank batch-size test makes its bounded group with
bounded_collectives, whose barrier keeps a rank from destroying the group while its peer is still connecting (one CI run had failed so); the batch sizing's distributed helpers are imported at module top; the rank-local refusal reads each algorithm's options through its mode's schema, so adistributed_syncof"false"is refused; the experts' groups are checked by the ranks they span, an expert-parallel group ofep_sizeranks and a data-parallel group ofedpranks meeting it only at the rank itself (a test answers four ranks' group queries, which the two-rank pool cannot build). Each behaviour fix fails its test without it (mutation-checked). Pre-commit passes against main; this PR's tests,test_distributed.py,test_quantize_cpu.pyand the hf_ptq argument tests pass on CPU with transformers 5.18 and 5.5.4.Review fix, 2026-10-29, second round:
awq_clip's per-tensor branch averages each candidate clip's loss over data parallelism, through the helper its static-block branch now uses too (same group, same op, every rank in the same order), andawq_clipis allowed on a distributed model; this also makes Megatron data-parallel replicas agree on the per-tensor clip. A two-rank CPU test of the static-block branch fails without the averaging (mutation-checked); a two-GPU NVFP4 test of the per-tensor branch is intests/gpu(NVFP4 fake quantization needs CUDA; not run here). The hf_ptq README and the changelog saynvfp4_awq_clipworks with--use_fsdp2on several GPUs. Pre-commit passes against main;test_calib.pyand this PR's tests pass on CPU.GPU check, 2026-10-29, of the NVFP4 per-tensor branch on GB200 (torch 2.11.0a0, transformers 5.9.0), with the second round's per-forward averaging; without the data-parallel mean the even and odd ranks pick different per-tensor clips:
test_calib_cuda.py::test_nvfp4_awq_clip_picks_the_same_clip_on_every_rankGPU check, 2026-10-29, at
cdb4e952468, with the fourth round's once-per-module averaging after the forward loop, on GB200 (torch 2.11.0a0, transformers 5.9.0); with that averaging disabled, the even and odd ranks pick different per-tensor clips again:cdb4e952468test_calib_cuda.py::test_nvfp4_awq_clip_picks_the_same_clip_on_every_ranksync_across_dpdisabled (returnas its first statement)GPU check, 2026-10-29, at
01a1df717e3, with the fifth round's empty-input guard, on GB200 (torch 2.11.0a0, transformers 5.9.0). The test now also runs a second linear on no tokens on the odd ranks; without the guard, that linear's clip stays 0 on every replica:01a1df717e3test_calib_cuda.py::test_nvfp4_awq_clip_picks_the_same_clip_on_every_rank_clip_searchamax[1] > 0: amax = [100, 0]Review fixes, 2026-10-29, third round: a
distributed_syncofNone, which skips the sync asFalsedoes, is refused too, nested scale algorithms included; the experts' groups are checked against the(edp, ep)mesh, the expert-parallel group as the rank's row in row order and the data-parallel group as its column, so diagonal data-parallel groups and a reordered expert-parallel group are refused (the four-rank test adds both). Each fails its test without the change (mutation-checked). Pre-commit passes against main; this PR's tests pass on CPU.Review fixes, 2026-10-29, fourth round:
awq_clipaverages its clip search over the data-parallel ranks once per module after the forward loop, with the token count, in both branches, instead of once per forward, which hung when ranks ran an MoE expert a different number of times (the two-rank test now does; it fails without the change, on its bounded group's timeout) (the same round accepted the experts' groups of a mesh with either axis innermost; the eighth round reverts that). Pre-commit passes against main;test_calib.pyand this PR's tests pass on CPU, and the quantization unit tests pass with the change.Review fix, 2026-10-29, fifth round:
awq_clipskips its clip search for an input with no tokens, which an MoE expert a rank routes none to can still get. Before, the per-tensor branch added NaN losses, which the post-loop sum spread to the replicas that had tokens (a clip of 0 for them), and the static-block branch failed to reshape the empty input. The two distributed tests now run an expert on no tokens on one rank, and check that its clips are positive and those a single process picks from the other ranks' tokens; each fails without the change (mutation-checked; the GPU one in the table above). Pre-commit passes against main;test_calib.pyand this PR's tests pass on CPU.Review fix, 2026-10-29, sixth round: hf_ptq's
--use_fsdp2plan recording, its README notes, changelog entry and argument test leave this PR for [OMNIML-6083] Write a sharded model's HF checkpoint without gathering it #2752 (see above); the new GPU testtests/gpu/torch/quantization/test_fsdp2.py::test_planned_batch_size_then_max_calibration(2+ GPUs) sizes an FSDP2 model's batch by its recorded plan with the realget_max_batch_size, rank 0's own estimate far above the others', checks every rank takes the smallest, then calibrates each rank's shard with max throughdistributed_forward_loopand checks the steps and scales agree. Its logic ran on two CPU ranks (FSDP2 on gloo, the CUDA memory queries faked): it passes, and without the batch size's reduction it fails at once on the sizes ([40, 4]); GPU results in the table below. Rebased onto main2b900c8fa41; this PR's own diff is otherwise unchanged. Pre-commit passes against main.GPU check, 2026-10-29, at
10b3c1f0e35, of the new FSDP2 test on GB200 (torch 2.11.0a0, CUDA 13.1, NCCL 2.29.2, transformers 5.9.0); without the cross-rank minimum, rank 0 keeps its own estimate while the others take theirs:10b3c1f0e35test_fsdp2.py::test_planned_batch_size_then_max_calibrationall_reducein the planned branchReview fix, 2026-10-29, seventh round: stacked on [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074 (rebased from main
2b900c8fa41onto [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074'sec9e09c3fc0; this PR's own diff is unchanged). New GPU testtests/gpu/torch/utils/test_moe_a2a.py::test_expert_parallel_calibration_matches_the_whole_model(2+ GPUs, transformers ≥ 5.16): a tiny Qwen3-MoE loaded by transformers' own expert-parallel load on thetorch_a2astyles and experts forward, quantized with FP8 throughdistributed_forward_loop, each rank on its own batch; every expert's weight scales and every activation scale match the whole model calibrated on every rank's batch, with bounded collectives. Its logic ran on two CPU ranks with transformers 5.18: it passes, re-planting the forward wrappers quantizer insertion drops, and fails without the re-plant, or with the conversion forcing eager as before [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074. GPU results in the table below. Pre-commit passes against [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074 and main.GPU check, 2026-10-29, at
d90a479b6b5, of the new expert-parallel test on GB200 (torch 2.11.0a0, CUDA 13.1, NCCL 2.29.2, transformers 5.18.0): the expert-parallel calibration matches the whole model's, and needs both the forward re-plant and [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074:d90a479b6b5test_moe_a2a.py::test_expert_parallel_calibration_matches_the_whole_model_reinstall_a2a_forwardsreturns 0)process_group"normalize_experts_impl_on_the_flyforcing eager, main's behaviour)Restacked on 2026-10-29 onto [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074 at
f68837967d3, now [2/2] on [OMNIML-6073] [1/2] Calibrate and fold expert-parallel fused experts on each rank's own experts #3122 [1/2] after [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074's split; no conflicts, and this PR's own diff is unchanged. The new expert-parallel test's logic still passes on two CPU ranks on this base. Pre-commit passes against [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074's head.GPU check, 2026-10-29, at
54f0c1fd8e9(on [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074f68837967d3, on [OMNIML-6073] [1/2] Calibrate and fold expert-parallel fused experts on each rank's own experts #312233736db61fa), on GB200 (torch 2.11.0a0, transformers 5.18.0): the expert-parallel calibration still matches the whole model with [OMNIML-6073] [1/2] Calibrate and fold expert-parallel fused experts on each rank's own experts #3122's per-rank expert calibration underneath:54f0c1fd8e9test_moe_a2a.py::test_expert_parallel_calibration_matches_the_whole_modelReview fixes, 2026-10-29, eighth round (Claude's review of
54f0c1fd8e9): the experts' groups are checked against the ep-innermost(edp, ep)mesh only, the layout the expert-parallel load builds: accepting either axis innermost let transposed groups pass whenedp == ep_size(a new four-rank case,groups-transposed, fails with that check).awq_clipreduces each module's clip search in one all-reduce, the token count riding in the losses' buffer (the ninth round reduces the losses in place instead), and every module's reduction runs before any weight gather. Pre-commit passes against [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074;test_calib.pyand this PR's tests pass on CPU, and dropping the token count's sync fails the two-rank test.On GPUs, with the rest of the expert-parallel series on top (transformers 5.13), before this round's change to
awq_clip:Qwen3-30B-A3B at EP 4 and EP 2 on 4 GPUs, deterministic, batch size 1: every exported scale is bit-identical between the two. In the EP 2 run (edp 2), the same row/column check, reading the groups from the field that stack's loader seeds, accepted the loader's real
(edp, ep)groups.weight_scaleinput_scalek_scalev_scaleQwen3-0.6B with
nvfp4_awq_clipand--use_fsdp2on 2 GPUs:awq_clipran and the sharded checkpoint was exported, end to end. That run had the per-forward averaging this round replaces.Review fixes, 2026-10-29, ninth round (Claude's review of
2dfeb9c23f0):awq_clip's candidate losses are views of one buffer, which the sync all-reduces and averages in place, with the token count in its own int64 all-reduce: no copy of the losses, where the packed buffer held up to three, and every module's reductions still run before any weight gather. The refusal namesdistributed_synconly for max and MSE, the calibrations that have it. Pre-commit passes against [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074;test_calib.pyand this PR's tests pass on CPU, and the two-rank test fails without either all-reduce, or with the candidates' losses copied out of the buffer.Review follow-up, 2026-10-29, tenth round (Claude's review of
b517c7581b6): a docstring fix (07affa0fe76). The new expert-parallel test's job, run on two CPU ranks with transformers 5.18, shows that its dense linears areQuantLinearwith the seeded data-parallel group and no tensor-parallel group (Qwen3-MoE's expert plan shards no dense module). Seeding them without that group fails the test:o_proj's input amax is off by 0.293 (relative).Review follow-up, 2026-10-29, eleventh round (Claude's review of
07affa0fe76): a docstring note (e546bcca0b1) that every rank still reads and decodes every sample of a sharded stream. The experts' check thatquantize()runs before conversion reads the groups a loader seeds. [OMNIML-6060] [3/3] Refuse an expert-parallel load that would calibrate the experts wrong #2741's load is the first to seed them; until it lands, the unit tests and the expert-parallel GPU test, which seed them by hand, exercise that check.Rebased on 2026-10-30 onto main
99c3834f62cthrough [OMNIML-6073] [1/2] Calibrate and fold expert-parallel fused experts on each rank's own experts #31221e74c31b49aand [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #30743f3544435d8(from [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074'sf5d36d4f794), asfd036d44f28; no conflicts, and this PR's own diff is unchanged. Pre-commit passes against main.GPU check, 2026-10-30, at
fd036d44f28, on GB200 (torch 2.11.0a0, transformers 5.9.0):awq_clip's in-place data-parallel sync (the ninth round's change) still gives every rank the same clip:fd036d44f28test_calib_cuda.py::test_nvfp4_awq_clip_picks_the_same_clip_on_every_rankRestacked on 2026-10-30 after [OMNIML-6073] [1/2] Calibrate and fold expert-parallel fused experts on each rank's own experts #3122 merged: onto [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074
39697ad985b, which is on main55898ee4e3b(from3f3544435d8), as3f56acfbb76; no conflicts, and this PR's own diff is unchanged. Pre-commit passes against main. This PR's unit tests (test_distributed_calibration.py,test_calib.py,test_dataset_utils.py,test_distributed.py) pass on CPU with transformers 5.18 and 5.5.4, and the expert-parallel GPU test's job passes on two CPU ranks with transformers 5.18.Rebased on 2026-10-30 onto [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074
a6758d77eb3, on main059588b89a5, which has [OMNIML-6081] Fix the FSDP2 export of W4A8 (SequentialQuantizer) models, and refuse compressing them #3128 (from3f56acfbb76), as186efae02e5. [OMNIML-6081] Fix the FSDP2 export of W4A8 (SequentialQuantizer) models, and refuse compressing them #3128's AWQ guard (_refuse_awq_on_a_sharded_fsdp2_model) and this PR's (_refuse_unsupported_distributed_calibration) met at the same three call sites inmodel_quant.py:calibrate(),quantize()andauto_quantize's candidate check. Both stay, [OMNIML-6081] Fix the FSDP2 export of W4A8 (SequentialQuantizer) models, and refuse compressing them #3128's first. The changelog and the README keep both sides, and186efae02e5makes this PR'snvfp4_awq_clipREADME note its own paragraph, saying why it is not refused. This PR's own diff is otherwise unchanged. Pre-commit passes against main. On CPU with transformers 5.18 and 5.5.4,test_quantize_cpu.py(with [OMNIML-6081] Fix the FSDP2 export of W4A8 (SequentialQuantizer) models, and refuse compressing them #3128's guard test) and this PR's unit tests pass, and the expert-parallel GPU test's job passes on two CPU ranks with transformers 5.18.Rebased on 2026-10-30 onto main
d464d30b120after [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074 merged ([OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074's squash; from186efae02e5), as9a87d153c89: [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074's own commits drop out; no conflicts, and this PR's own diff is unchanged, with [OMNIML-6081] Fix the FSDP2 export of W4A8 (SequentialQuantizer) models, and refuse compressing them #3128's guard still first at the three call sites. This PR now targets main, so the full CI runs, GPU included. Pre-commit passes against main. On CPU with transformers 5.18 and 5.5.4,test_quantize_cpu.pyand this PR's unit tests pass, and the expert-parallel GPU test's job passes on two CPU ranks with transformers 5.18.Review fixes, 2026-10-30, twelfth round (cjluo-nv's review of
9a87d153c89):mtq.auto_quantizerefuses a model whose recorded plan shards its experts, before converting it (fbbe2275cf8); a two-rank test with seeded experts and an INT8 candidate checks the module types and state are unchanged, and fails without the refusal.shard_dataloadercompares a sampler's source with the loader's dataset (e1976572627, typed in9ac1486cdab); a SequentialSampler, RandomSampler or DistributedSampler over a shorter source is refused, and the three new cases fail with the old check. Pre-commit passes against main. On CPU with transformers 5.18 and 5.5.4,test_quantize_cpu.pyand this PR's unit tests pass, and the expert-parallel GPU test's job passes on two CPU ranks with transformers 5.18.Review fix, 2026-10-31, thirteenth round (Claude's review of
9ac1486cdab): the forward re-plant now plants only over a bound method, the forward quantizer insertion leaves, as its docstring says (aba16882660); a planned module whose forward the load never wrapped is left alone, which a new test checks and the old guard fails. Pre-commit passes against main. On CPU with transformers 5.18 and 5.5.4,test_quantize_cpu.pyand this PR's unit tests pass, and the expert-parallel GPU test's job still re-plants the two experts' wrappers and passes on two CPU ranks with transformers 5.18.Rebased on 2026-10-31 onto main
e352caca3c3(fromaba16882660, adding Run fused-MoE expert GPTQ updates on all of this process's GPUs #3002, Collect imatrix statistics with one fused float32 reduction #3044 and [OMNIML-6273] Let the Claude review bot submit its review again #3156), asdea9b793553; no conflicts (Collect imatrix statistics with one fused float32 reduction #3044 and Run fused-MoE expert GPTQ updates on all of this process's GPUs #3002 touchmodel_calib.py, which merges cleanly), and this PR's own diff is unchanged. Pre-commit passes against main. On CPU with transformers 5.18 and 5.5.4,test_quantize_cpu.py, the ggml GPTQ and imatrix unit tests and this PR's unit tests pass, and the expert-parallel GPU test's job passes on two CPU ranks with transformers 5.18.Before your PR is "Ready for review"
get_max_batch_sizeand the calibration refusals change only for a model whose recorded plan spans several ranks.awq_clip's per-tensor (NVFP4) clip changes under data parallelism, where its replicas now agree (changelog, Bug Fixes).CONTRIBUTING.md: N/AAdditional Information
Part 8 of 21 of expert-parallel PTQ for Hugging Face MoE models; merges after #2739, which has merged.
Summary by CodeRabbit