Skip to content

Commit 0cb804b

Browse files
Fridah-nvclaude
andcommitted
refactor(export): declare the module's dependencies at the top
layerwise_export.py had 17 function-local imports, none of them cycle-driven: nothing under export/ imports this module, and export/__init__ does not either, so the deferral bought nothing that CONTRIBUTING's module-top rule asks us to justify. Verified by importing the module from both entry orders and running the export suites. The one that stays local is plugins.huggingface, which pulls in transformers -- unified_export_hf, unified_export_hf_streaming and moe_utils all defer that same import, so this now matches its neighbours. Separately, _reconcile_export_with_resume moves to utils/layerwise_calib.py, next to the manifest reader and detect_resume_point whose contract it encodes, keeping model_calib.py to the algorithms. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Fridah-nv <201670829+Fridah-nv@users.noreply.github.com>
1 parent 8fbb734 commit 0cb804b

3 files changed

Lines changed: 58 additions & 71 deletions

File tree

modelopt/torch/export/layerwise_export.py

Lines changed: 26 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,34 @@
2222

2323
import torch
2424
import torch.nn as nn
25+
from safetensors import safe_open
2526
from safetensors.torch import save_file
2627

28+
from modelopt.torch.quantization.nn import SequentialQuantizer, TensorQuantizer
29+
from modelopt.torch.quantization.utils.core_utils import (
30+
enable_weight_access_and_writeback,
31+
requires_weight_materialization,
32+
)
33+
from modelopt.torch.quantization.utils.layerwise_calib import LayerActivationCollector
34+
from modelopt.torch.utils import distributed as dist
35+
36+
from .layer_utils import is_moe, sync_moe_gate_up_amax
2737
from .model_config import FUSION_FREE_FORMATS, QUANTIZATION_NVFP4
28-
from .quant_utils import get_quant_config, get_quantization_format
38+
from .model_utils import TiedWeightMap
39+
from .quant_aware_conversion import build_reverse_name_mapper, revert_quant_config_names
40+
from .quant_utils import _postprocess_single_tensor, get_quant_config, get_quantization_format
41+
from .registry import ExportContext, PrepareMoEInputsRegistry
42+
from .unified_export_hf import (
43+
_add_mtp_exclusions,
44+
_dispatch_export_handler,
45+
_fuse_shared_input_modules,
46+
_prepare_moe_inputs,
47+
_resolve_export_dtype,
48+
_write_hf_export_config,
49+
collect_shared_input_modules,
50+
save_non_weight_artifacts,
51+
)
52+
from .unified_export_hf_streaming import _assert_no_split_rules
2953

3054
# Fusable per layer because the groups (q/k/v, gate/up) never cross a decoder boundary.
3155
# AWQ and SVDQuant also need pre-quant-scale steps, which are still whole-model.
@@ -44,8 +68,6 @@ def layer_shard_name(layer_idx: int) -> str:
4468

4569
def _is_quantized_module(module: nn.Module) -> bool:
4670
"""By type, not name: fused experts name theirs ``gate_up_proj_weight_quantizer``."""
47-
from modelopt.torch.quantization.nn import SequentialQuantizer, TensorQuantizer
48-
4971
return any(
5072
isinstance(child, (TensorQuantizer, SequentialQuantizer)) for child in module.children()
5173
)
@@ -67,8 +89,6 @@ def _tied_quantized_modules(model: nn.Module) -> list[str]:
6789
weights are on meta and would pass vacuously. Falls back to ``data_ptr`` when the model
6890
publishes no map (transformers < 5).
6991
"""
70-
from .model_utils import TiedWeightMap
71-
7292
tied_map = TiedWeightMap(model)
7393
groups: dict[str, list[str]] = {}
7494
by_ptr: dict[int, list[str]] = {}
@@ -105,8 +125,6 @@ def assert_formats_supported(module: nn.Module, scope: str) -> None:
105125

106126
def assert_layerwise_export_supported(model: nn.Module) -> None:
107127
"""Raise unless per-layer export is valid for this model."""
108-
from modelopt.torch.utils import distributed as dist
109-
110128
assert_formats_supported(model, "before calibration")
111129

112130
tied = _tied_quantized_modules(model)
@@ -145,14 +163,6 @@ def __init__(
145163
146164
Runs before calibration, so nothing amax-dependent exists yet.
147165
"""
148-
from modelopt.torch.quantization.utils.layerwise_calib import LayerActivationCollector
149-
150-
from .layer_utils import is_moe
151-
from .quant_aware_conversion import build_reverse_name_mapper
152-
from .registry import ExportContext, PrepareMoEInputsRegistry
153-
from .unified_export_hf import _resolve_export_dtype
154-
from .unified_export_hf_streaming import _assert_no_split_rules
155-
156166
assert_layerwise_export_supported(model)
157167
# Splits regroup tensors across the whole state dict; no per-layer pass reverses that.
158168
_assert_no_split_rules(model)
@@ -214,10 +224,9 @@ def export_layer(
214224
a fusing format can rediscover which modules share an input; omit them only when
215225
nothing fuses.
216226
"""
227+
# Local, as in every other export module: the plugin imports transformers.
217228
from modelopt.torch.quantization.plugins.huggingface import _reconstruct_fused_moe_linear
218229

219-
from .unified_export_hf import _dispatch_export_handler, _prepare_moe_inputs
220-
221230
assert not self._finalized, "export_layer() called after finalize()"
222231
if layer_module is not self._layers[layer_idx]:
223232
# Not an assert: -O would strip it, and the failure is silent -- layer N's
@@ -257,8 +266,6 @@ def _unify_shared_quantization_params(
257266
shared-input group, one weight_scale_2 per expert gate/up pair. Its pre-quant-scale
258267
steps are AWQ/SVDQuant-only and refused.
259268
"""
260-
from .layer_utils import sync_moe_gate_up_amax
261-
262269
# A set, not get_quantization_format: that stops at the first hit, so a mixed
263270
# FP8-attention/NVFP4-expert layer would report fp8 and skip fusing entirely.
264271
if _module_formats(layer_module) - FUSION_FREE_FORMATS:
@@ -267,9 +274,6 @@ def _unify_shared_quantization_params(
267274

268275
def _fuse_shared_input_scales(self, layer_module: nn.Module, layer_inputs: list | None) -> None:
269276
"""Rediscover the groups that share an input, on real activations, and fuse them."""
270-
from .quant_utils import get_quantization_format
271-
from .unified_export_hf import _fuse_shared_input_modules, collect_shared_input_modules
272-
273277
layer_format = get_quantization_format(layer_module)
274278
if not layer_inputs:
275279
raise RuntimeError(
@@ -290,19 +294,6 @@ def finalize(self) -> dict:
290294
291295
Leaves ``export_dir`` a complete checkpoint; no ``export_hf_checkpoint()`` needed.
292296
"""
293-
from modelopt.torch.quantization.utils.core_utils import (
294-
enable_weight_access_and_writeback,
295-
requires_weight_materialization,
296-
)
297-
298-
from .quant_aware_conversion import revert_quant_config_names
299-
from .unified_export_hf import (
300-
_add_mtp_exclusions,
301-
_dispatch_export_handler,
302-
_write_hf_export_config,
303-
save_non_weight_artifacts,
304-
)
305-
306297
assert not self._finalized, "finalize() called twice"
307298
self._finalized = True
308299

@@ -407,8 +398,6 @@ def assert_shards_present(self, upto: int) -> None:
407398

408399
def _collect(self, out: dict[str, torch.Tensor], full_key: str, tensor: torch.Tensor) -> None:
409400
"""Apply per-tensor export postprocessing and hub-name reversal, or drop the tensor."""
410-
from .quant_utils import _postprocess_single_tensor
411-
412401
if tensor is None or tensor.is_meta:
413402
return
414403
new_key, new_value = _postprocess_single_tensor(
@@ -426,8 +415,6 @@ def _write_index(self) -> None:
426415
From disk because a resumed run never saw the earlier shards in memory; by layer
427416
count rather than a glob, so a longer previous run's leftovers cannot leak in.
428417
"""
429-
from safetensors import safe_open
430-
431418
# Out of the index already; delete them so the directory *is* the checkpoint.
432419
for stale in self._export_dir.glob("model-layer-*.safetensors"):
433420
if int(stale.stem.rsplit("-", 1)[1]) >= len(self._layers):

modelopt/torch/quantization/model_calib.py

Lines changed: 1 addition & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
from modelopt.torch.quantization.utils.layerwise_calib import (
3535
LayerActivationCollector,
3636
_CheckpointState,
37-
_read_manifest,
37+
_reconcile_export_with_resume,
3838
)
3939
from modelopt.torch.utils import print_rank_0, warn_rank_0
4040
from modelopt.torch.utils.distributed import DistributedProcessGroup, ParallelState, is_master
@@ -2048,37 +2048,6 @@ def postprocess(module, name):
20482048
max_calibrate(model, forward_loop)
20492049

20502050

2051-
def _reconcile_export_with_resume(
2052-
exporter, checkpoint_dir: str | None, start_layer: int, num_layers: int
2053-
) -> bool:
2054-
"""Reconcile the shards on disk with the layer calibration will start from.
2055-
2056-
Returns True when every layer already has a shard, so only ``finalize()`` is owed.
2057-
"""
2058-
manifest = _read_manifest(checkpoint_dir) if checkpoint_dir is not None else None
2059-
last = (manifest or {}).get("last_completed_layer")
2060-
total = (manifest or {}).get("num_layers")
2061-
if total is not None and total != num_layers:
2062-
raise ValueError(
2063-
f"Layerwise checkpoint at {checkpoint_dir} was written for {total} layers "
2064-
f"but this model has {num_layers}. Use a fresh checkpoint_dir."
2065-
)
2066-
# detect_resume_point returns None once the manifest is complete, which puts start_layer
2067-
# back at 0 and would recalibrate everything the shards already hold.
2068-
if last is not None and last + 1 >= num_layers:
2069-
exporter.assert_shards_present(num_layers)
2070-
return True
2071-
2072-
if start_layer > 0:
2073-
exporter.assert_shards_present(start_layer)
2074-
elif checkpoint_dir is not None:
2075-
# Starting at 0 with a checkpoint_dir means no usable resume record, so calibration
2076-
# would silently overwrite finished shards. Without one there is no resume to lose,
2077-
# and re-exporting is the documented behaviour.
2078-
exporter.assert_no_orphan_shards()
2079-
return False
2080-
2081-
20822051
@torch.no_grad()
20832052
def layerwise_calibrate(
20842053
model: nn.Module,

modelopt/torch/quantization/utils/layerwise_calib.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,37 @@ def detect_resume_point(checkpoint_dir: str) -> tuple[int, dict] | None:
566566
return (last + 1, manifest)
567567

568568

569+
def _reconcile_export_with_resume(
570+
exporter, checkpoint_dir: str | None, start_layer: int, num_layers: int
571+
) -> bool:
572+
"""Reconcile the shards on disk with the layer calibration will start from.
573+
574+
Returns True when every layer already has a shard, so only ``finalize()`` is owed.
575+
"""
576+
manifest = _read_manifest(checkpoint_dir) if checkpoint_dir is not None else None
577+
last = (manifest or {}).get("last_completed_layer")
578+
total = (manifest or {}).get("num_layers")
579+
if total is not None and total != num_layers:
580+
raise ValueError(
581+
f"Layerwise checkpoint at {checkpoint_dir} was written for {total} layers "
582+
f"but this model has {num_layers}. Use a fresh checkpoint_dir."
583+
)
584+
# detect_resume_point returns None once the manifest is complete, which puts start_layer
585+
# back at 0 and would recalibrate everything the shards already hold.
586+
if last is not None and last + 1 >= num_layers:
587+
exporter.assert_shards_present(num_layers)
588+
return True
589+
590+
if start_layer > 0:
591+
exporter.assert_shards_present(start_layer)
592+
elif checkpoint_dir is not None:
593+
# Starting at 0 with a checkpoint_dir means no usable resume record, so calibration
594+
# would silently overwrite finished shards. Without one there is no resume to lose,
595+
# and re-exporting is the documented behaviour.
596+
exporter.assert_no_orphan_shards()
597+
return False
598+
599+
569600
class _CheckpointState:
570601
"""Manages checkpoint save and restore for layerwise calibration.
571602

0 commit comments

Comments
 (0)