Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
4343c9d
initial refactor
sugunav14 May 21, 2026
d78b547
cleanup
sugunav14 May 22, 2026
a3d1f8b
prototype, untested
sugunav14 May 26, 2026
8d39037
tested layerwise + non layerwise
sugunav14 May 28, 2026
ce66d36
added and tested CPU offloading policy
sugunav14 May 29, 2026
9b90b4e
added process parallel loading
sugunav14 Jun 2, 2026
a86b744
claude self-review comments
sugunav14 Jun 2, 2026
d9ad10f
coderabbit reviews
sugunav14 Jun 2, 2026
08dcd95
modelopt bot review
sugunav14 Jun 2, 2026
b783aff
clean up (1)
sugunav14 Jun 2, 2026
f9b4e45
refactor
sugunav14 Jun 4, 2026
cec0305
claude self review
sugunav14 Jun 4, 2026
c4f34a1
modelopt bot review
sugunav14 Jun 4, 2026
d2e4c1a
minor update
sugunav14 Jun 4, 2026
6f786f9
updated README
sugunav14 Jun 4, 2026
5906bf6
removed load_and_prepare_fsdp2 wrapper
sugunav14 Jun 4, 2026
5eb2c25
PR review comments
sugunav14 Jun 5, 2026
1bf9ebb
modified parallel read to do one broadcast per rank instead of one pe…
sugunav14 Jun 5, 2026
86e05cc
PR review addressed
sugunav14 Jun 22, 2026
b549e62
perf measurements, revert later
sugunav14 Jul 5, 2026
88508ba
refactor + perf measured
sugunav14 Jul 16, 2026
9bfecd1
cleanup
sugunav14 Jul 16, 2026
0831244
shard_root=True fix
sugunav14 Jul 18, 2026
4a83d15
added slurm example and updated README.md
sugunav14 Jul 18, 2026
ca924ff
code quality check
sugunav14 Jul 19, 2026
a9cb58a
fix unit tests
sugunav14 Jul 19, 2026
88a9145
updated unit test for shard_root=True default
sugunav14 Jul 19, 2026
8abf326
export fix
sugunav14 Jul 20, 2026
b336a64
updated agent skills with recipe
sugunav14 Jul 20, 2026
f449c10
updated dedup comment
sugunav14 Jul 20, 2026
2ed2a20
FSDP2 shard-local NVFP4 weight-packing for HF export
sugunav14 Jul 23, 2026
2eb28f9
FSDP2 per-rank parallel checkpoint write
sugunav14 Jul 23, 2026
6fa6d0e
Remove env-var toggles and debug instrumentation from FSDP2 export
sugunav14 Jul 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 15 additions & 12 deletions .agents/skills/ptq/references/slurm-setup-ptq.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,23 +63,26 @@ pip install -U transformers --no-deps

Estimate GPU count from model size and available GPU memory. `hf_ptq.py` uses `device_map="auto"` so it fills GPUs automatically — request only as many as needed.

For multi-node PTQ (200B+ params), use `examples/hf_ptq/multinode_ptq.py` with FSDP2 and accelerate:
For multi-node PTQ (200B+ params), use `hf_ptq.py --use_fsdp2` launched via `torchrun`:

```bash
accelerate launch \
--config_file examples/hf_ptq/fsdp2.yaml \
--num_machines $NUM_NODES \
--num_processes $((NUM_NODES * GPUS_PER_NODE)) \
--main_process_ip $MASTER_ADDR \
--main_process_port $MASTER_PORT \
--machine_rank $SLURM_PROCID \
examples/hf_ptq/multinode_ptq.py \
torchrun \
--nnodes=$NUM_NODES \
--node_rank=$SLURM_PROCID \
--nproc_per_node=$GPUS_PER_NODE \
--master_addr=$MASTER_ADDR \
--master_port=$MASTER_PORT \
examples/hf_ptq/hf_ptq.py \
--pyt_ckpt_path <model> \
--qformat <format> \
--export_path <output>
--recipe <recipe> \
--export_path <output> \
--use_fsdp2
```

The `num_machines`, `num_processes`, `main_process_ip`, and `machine_rank` are overridden on the command line — no need to edit `fsdp2.yaml`. Only update `fsdp_transformer_layer_cls_to_wrap` in the YAML if the model uses a non-default decoder layer class.
`--recipe` is the maintained entry point (e.g. `general/ptq/nvfp4_default-kv_fp8_cast`); see the
Recipe-based Quantization section of `examples/hf_ptq/README.md` for the format and built-in names.

When the per-rank decoder shard approaches GPU capacity (200B+ at low rank count), either add more nodes (more ranks → smaller shard per rank) or add `--cpu_offload`. Layer detection is automatic; no YAML config needed.

Use the multi-node template from `skills/common/slurm-setup.md` section 4 as the job script wrapper.

Expand Down
34 changes: 19 additions & 15 deletions examples/hf_ptq/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -428,33 +428,37 @@ mtq.calibrate(model, algorithm="max", forward_loop=calibrate_loop)

## Multi-Node Post-Training Quantization with FSDP2

ModelOpt enables quantization of LLMs across multiple GPU nodes using various quantization formats. It leverages HuggingFace's Accelerate library and FSDP2 for distributed model sharding and calibration.
ModelOpt enables quantization of LLMs across multiple GPU nodes using FSDP2 for distributed model sharding and calibration, exposed via the `--use_fsdp2` flag on the standard `hf_ptq.py` entry point.

### Usage

For distributed execution across multiple nodes, use the `accelerate` library. A template configuration file (`fsdp2.yaml`) is provided and can be customized for user specific requirements.
#### Slurm (recommended)

On each node run the following command:
Slurm orchestrates launching the job on every node for you, so this is the easiest way to run a multi-node PTQ. A ready-to-run example that quantizes Nemotron-3-Super to NVFP4 is provided in [`slurm/multinode_fsdp2_ptq.slurm`](./slurm/multinode_fsdp2_ptq.slurm). Edit the `CONFIG` block (container image, model path, export path, recipe) and submit:

```bash
accelerate launch --config_file fsdp2.yaml \
--num_machines=<num_nodes> \
--machine_rank=<current_node_rank> \
--main_process_ip=<node0_ip_addr> \
--main_process_port=<port> \
--fsdp_transformer_layer_cls_to_wrap=<decoder_layer_name>
multinode_ptq.py \
sbatch --nodes=2 slurm/multinode_fsdp2_ptq.slurm
```

#### Manual (run on each node)

Without Slurm, start `torchrun` on every node yourself:

```bash
torchrun \
--nnodes=<num_nodes> --node_rank=<current_node_rank> \
--master_addr=<node0_ip_addr> --master_port=<port> \
--nproc_per_node=<num_gpus_per_node> \
hf_ptq.py \
--pyt_ckpt_path <path_to_model> \
--qformat <fp8/nvfp4/nvfp4_mlp_only/nvfp4_experts_only/nvfp4_omlp_only/nvfp4_awq/int8> \
--kv_cache_qformat <fp8/nvfp4/nvfp4_affine/none> \
--recipe general/ptq/nvfp4_default-kv_fp8_cast \
--batch_size <calib_batch_size> \
--calib_size <num_calib_samples> \
--dataset <dataset> \
--export_path <export_path> \
--trust_remote_code
--use_fsdp2
```

The exported checkpoint can be deployed using TensorRT-LLM/ vLLM/ SGLang. For more details refer to the [deployment section](#deployment) of this document.
See [Recipe-based Quantization](#recipe-based-quantization) for the recipe format and built-in recipe names. The exported checkpoint can be deployed using TensorRT-LLM/ vLLM/ SGLang. For more details refer to the [deployment section](#deployment) of this document.

> *Performance Note: FSDP2 is designed for training workloads and may result in longer calibration and export times. For faster calibration, maximize the batch size based on available GPU memory and choose the right number of GPUs to avoid unnecessary communication.*

Expand Down
57 changes: 57 additions & 0 deletions examples/hf_ptq/example_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,55 @@
except ImportError:
snapshot_download = None

from modelopt.torch.utils import distributed as dist_utils

logger = logging.getLogger(__name__)

SPECULATIVE_MODEL_LIST = ["Eagle", "Medusa"]


def setup_distributed_args(args):
"""Set ``args.rank``/``world_size``/``device``/``is_main`` (single-process if FSDP2 off)."""
if getattr(args, "use_fsdp2", False):
dist_utils.setup()
args.rank = dist_utils.rank()
args.world_size = dist_utils.size()
args.device = torch.device(f"cuda:{dist_utils.local_rank()}")
args.is_main = args.rank == 0
else:
args.rank = 0
args.world_size = 1
args.is_main = True


def cleanup_distributed(args):
"""Destroy the process group if ``--use_fsdp2`` set it up."""
if getattr(args, "use_fsdp2", False):
dist_utils.cleanup()


def validate_fsdp2_supported(args, config):
"""Raise ``NotImplementedError`` for model/CLI combos the FSDP2 path doesn't support yet."""
issues = []
if "vila" in args.pyt_ckpt_path.lower():
issues.append("VILA (custom builder + non-standard layer layout)")
if is_nemotron_vl(config) or _is_multimodal_config(config):
issues.append("multimodal / VL models (decoder layers not auto-detectable)")
if getattr(config, "quantization_config", None) is not None:
issues.append("pack-quantized / compressed-tensors checkpoints")
if getattr(args, "specdec_offline_dataset", None) is not None:
issues.append("speculative decoding (--specdec_offline_dataset)")
if getattr(args, "low_memory_mode", False):
issues.append("--low_memory_mode (redundant with FSDP2)")

if issues:
raise NotImplementedError(
"--use_fsdp2 does not support:\n - "
+ "\n - ".join(issues)
+ "\nRemove --use_fsdp2 or use a standard causal-LM checkpoint."
)


def run_nemotron_vl_preview(
full_model,
tokenizer,
Expand Down Expand Up @@ -372,6 +416,19 @@ def _apply_to_model_state_dict(
return out_state_dict


def mtp_layer_prefixes_from_checkpoint(model_path: str) -> list[str]:
"""MTP exclude-prefixes from a checkpoint's safetensors index (``[]`` if none); reads no tensors.

Local-index-only, matching :func:`load_mtp_weights`, so detection and re-attach stay in sync.
"""
index_file = Path(model_path) / "model.safetensors.index.json"
if not index_file.exists():
return []
weight_map = json.load(open(index_file))["weight_map"]
mtp_keys = [k for k, v in weight_map.items() if "mtp" in k or "mtp" in v]
return list(_keys_to_prefixes(mtp_keys))


def load_mtp_weights(
model: torch.nn.Module, model_path: str
) -> tuple[list[str], dict[str, torch.Tensor]]:
Expand Down
30 changes: 0 additions & 30 deletions examples/hf_ptq/fsdp2.yaml

This file was deleted.

Loading