Skip to content

Commit 302a7ad

Browse files
juhi10071998claude
andauthored
[NVBug: 6524370] use sequential device_map for DiffusionGemma (#2041)
### What does this PR do? Type of change: Bug fix Fixes NVBug 6524370. `DiffusionGemma` ties weights between its encoder and decoder. `get_model` loads with `device_map="auto"` (`examples/hf_ptq/example_utils.py`), and `"auto"` is an alias for `"balanced"` — accelerate splits the model evenly across all visible GPUs by size, with no awareness of tied parameters. On multi-GPU it can place the two sides of a tied pair on different devices; the tie then cannot be honored and one side is left on the `meta` device. The pre-quantization preview in `pre_quantize` then reaches `(input_ids == self.config.image_token_id).any()` in `generation_diffusion_gemma.py` and fails: ``` RuntimeError: Tensor.item() cannot be called on meta tensors ``` This is multi-GPU-only by construction: with one visible GPU the balanced split is trivial, nothing is separated, and nothing lands on `meta`. This PR detects DiffusionGemma configs in `get_model` and selects `device_map="sequential"`, which fills one GPU before spilling to the next and so keeps tied modules together. It mirrors the existing per-model handling for `bart` and `t5`, where `device_map="auto"` similarly mis-shards tied encoder/decoder weights. Detection reads `model_type` and `architectures` from the config and ignores underscores, since the family is spelled `diffusion_gemma` in the Transformers module path and `DiffusionGemma` in the class name. ### Usage No API change. Previously this needed the flag passed manually: ```bash python hf_ptq.py --model <diffusion-gemma-ckpt> --recipe <recipe> \ --export_path <out> --trust_remote_code --use_seq_device_map ``` It is now selected automatically, and the model load logs: ``` Detected DiffusionGemma model. Using device_map='sequential'; the balanced 'auto' mapping can split its tied encoder/decoder weights across GPUs. ``` Passing `--use_seq_device_map` explicitly still works and is unaffected. ### Testing - Reproduced on 4x GB200 with `diffusiongemma-26B-A4B-it` and the `nvfp4_experts_only` recipe; `--use_seq_device_map` resolves the crash, confirming the device-mapping cause. - Validated on oci-hsg (4x GB200): with this patch and no CLI flag, `diffusiongemma-26B-A4B-it` loads correctly and the meta-tensor crash no longer reproduces. - `is_diffusion_gemma` checked against both config spellings, `architectures=None`, `architectures=[]`, and a `gemma3` negative to confirm no over-match — `get_model_type` already orders `DiffusionGemma` before `Gemma` for exactly this substring-collision reason. - `pre-commit run --files examples/hf_ptq/example_utils.py` passes (ruff, ruff-format, mypy, bandit). ### Before your PR is "*Ready for review*" - Is this change backward compatible?: ✅ - 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?: ❌ — no existing unit coverage for `get_model` device-map selection; happy to add a config-level test for `is_diffusion_gemma` if wanted. - Did you update [Changelog](https://github.com/NVIDIA/Model-Optimizer/blob/main/CHANGELOG.rst)?: N/A - Did you get Claude approval on this PR?: ❌ — pending ### Additional Information NVBug 6524370. Same class of failure as the existing `t5` workaround in `get_model`; a general "any tied encoder/decoder model" rule was considered but rejected, since `tie_word_embeddings=True` holds for most decoder-only LLMs where `auto` is fine and forcing sequential would regress large-model runs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved DiffusionGemma model loading on multi-GPU systems by keeping related model weights together. * Added more reliable DiffusionGemma model recognition across supported configurations. * Preserved existing automatic device allocation for single-GPU systems and other supported models. * Improved loading reliability by applying appropriate memory limits during multi-GPU setup. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Signed-off-by: Juhi Mittal <juhim@nvidia.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 14b20c0 commit 302a7ad

2 files changed

Lines changed: 130 additions & 0 deletions

File tree

examples/hf_ptq/example_utils.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,20 @@ def is_speculative(hf_config):
304304
)
305305

306306

307+
def is_diffusion_gemma(hf_config) -> bool:
308+
"""Check if the model architecture is DiffusionGemma.
309+
310+
Underscores are ignored: the family is spelled ``diffusion_gemma`` in configs
311+
and ``DiffusionGemma`` in class names. The nested ``text_config`` is checked too,
312+
since multi-modal wrappers keep the family name there.
313+
"""
314+
names = []
315+
for cfg in (hf_config, getattr(hf_config, "text_config", None)):
316+
names.append(getattr(cfg, "model_type", None) or "")
317+
names.extend(getattr(cfg, "architectures", None) or [])
318+
return any("diffusiongemma" in name.lower().replace("_", "") for name in names)
319+
320+
307321
def get_tokenizer(ckpt_path, trust_remote_code=False, **kwargs) -> PreTrainedTokenizerBase:
308322
print(f"Initializing tokenizer from {ckpt_path}")
309323

@@ -696,6 +710,19 @@ def get_model(
696710
model_kwargs = config_kwargs.copy()
697711
model_kwargs.setdefault("dtype", "auto")
698712

713+
# DiffusionGemma ties encoder/decoder weights. device_map "auto" (balanced) can split
714+
# a tied pair across GPUs, leaving one side on the meta device and breaking generation.
715+
# Sequential packs the model onto GPU 0 first (up to gpu_mem_percentage), keeping tied
716+
# modules together for checkpoints that fit; larger ones can still spill and split a
717+
# tied pair, and need an explicit single-device map. Multi-GPU only: a single-GPU split
718+
# cannot separate a tied pair, and sequential would needlessly cap max_memory there.
719+
if device != "cpu" and torch.cuda.device_count() > 1 and is_diffusion_gemma(hf_config):
720+
print(
721+
"Detected DiffusionGemma model. Using device_map='sequential'; the balanced "
722+
"'auto' mapping can split its tied encoder/decoder weights across GPUs."
723+
)
724+
use_seq_device_map = True
725+
699726
if use_seq_device_map:
700727
device_map = "sequential"
701728
# If we use sequential, set max_memory limit to ensure that the model does not occupy the full GPU

tests/examples/hf_ptq/test_example_utils.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,3 +316,106 @@ def from_pretrained(*args, **kwargs):
316316
else:
317317
assert "trust_remote_code" not in calls["from_config"]
318318
assert calls["from_pretrained"]["trust_remote_code"] is True
319+
320+
321+
@pytest.mark.parametrize(
322+
("model_type", "architecture", "device_count", "expected_device_map"),
323+
[
324+
# DiffusionGemma ties encoder/decoder weights; "auto" can split a tied pair
325+
# across GPUs, so multi-GPU loads must fall back to "sequential".
326+
("diffusion_gemma", "DiffusionGemmaForConditionalGeneration", 2, "sequential"),
327+
# Detection must also work off ``architectures`` alone, without ``model_type``.
328+
(None, "DiffusionGemmaForConditionalGeneration", 2, "sequential"),
329+
# Single GPU cannot split a tied pair, so it keeps the unrestricted "auto" map.
330+
("diffusion_gemma", "DiffusionGemmaForConditionalGeneration", 1, "auto"),
331+
# "gemma" is a substring of "diffusiongemma"; other Gemmas must not match.
332+
("gemma3", "Gemma3ForCausalLM", 2, "auto"),
333+
],
334+
)
335+
def test_get_model_device_map_for_diffusion_gemma(
336+
monkeypatch, model_type, architecture, device_count, expected_device_map
337+
):
338+
calls = {}
339+
hf_config = SimpleNamespace(
340+
architectures=[architecture],
341+
dtype=torch.float16,
342+
model_type=model_type,
343+
torch_dtype=torch.bfloat16,
344+
)
345+
346+
class FakeModel:
347+
def eval(self):
348+
calls["eval"] = True
349+
350+
def parameters(self):
351+
return iter(())
352+
353+
class FakeArchitecture:
354+
@staticmethod
355+
def _from_config(config, **kwargs):
356+
return FakeModel()
357+
358+
@staticmethod
359+
def from_pretrained(*args, **kwargs):
360+
calls["from_pretrained"] = kwargs
361+
return FakeModel()
362+
363+
monkeypatch.setattr(
364+
example_utils.AutoConfig, "from_pretrained", lambda *args, **kwargs: hf_config
365+
)
366+
# Set rather than delete: ``transformers`` lazy-imports, so a deleted real class
367+
# (e.g. Gemma3ForCausalLM) reappears on the next ``hasattr`` and the real one loads.
368+
# raising=False: DiffusionGemma may not exist in the installed transformers.
369+
monkeypatch.setattr(example_utils.transformers, architecture, FakeArchitecture, raising=False)
370+
monkeypatch.setattr(example_utils, "is_nemotron_vl", lambda config: False)
371+
monkeypatch.setattr(example_utils, "is_speculative", lambda config: False)
372+
monkeypatch.setattr(example_utils, "init_empty_weights", lambda include_buffers: nullcontext())
373+
monkeypatch.setattr(example_utils, "get_max_memory", lambda: {0: 1024})
374+
monkeypatch.setattr(example_utils, "infer_auto_device_map", lambda model, max_memory: {"": 0})
375+
monkeypatch.setattr(torch.cuda, "device_count", lambda: device_count)
376+
377+
example_utils.get_model("checkpoint", device="cuda", trust_remote_code=True)
378+
379+
assert calls["from_pretrained"]["device_map"] == expected_device_map
380+
# Sequential caps per-GPU memory; "auto" must stay unrestricted.
381+
if expected_device_map == "sequential":
382+
assert calls["from_pretrained"]["max_memory"] == {0: 1024 * 0.8}
383+
else:
384+
assert "max_memory" not in calls["from_pretrained"]
385+
386+
387+
@pytest.mark.parametrize(
388+
("hf_config", "expected"),
389+
[
390+
(SimpleNamespace(model_type="diffusion_gemma", architectures=None), True),
391+
(SimpleNamespace(model_type=None, architectures=["DiffusionGemmaForCausalLM"]), True),
392+
(SimpleNamespace(model_type="gemma3", architectures=["Gemma3ForCausalLM"]), False),
393+
# Multi-modal wrappers keep the family name on the nested ``text_config``.
394+
(
395+
SimpleNamespace(
396+
model_type="multimodal",
397+
architectures=["SomeWrapperForConditionalGeneration"],
398+
text_config=SimpleNamespace(model_type="diffusion_gemma"),
399+
),
400+
True,
401+
),
402+
(
403+
SimpleNamespace(
404+
model_type="multimodal",
405+
text_config=SimpleNamespace(architectures=["DiffusionGemmaForCausalLM"]),
406+
),
407+
True,
408+
),
409+
# A non-DiffusionGemma nested config must not match.
410+
(
411+
SimpleNamespace(
412+
model_type="multimodal", text_config=SimpleNamespace(model_type="gemma3")
413+
),
414+
False,
415+
),
416+
# Stub configs may omit either attribute entirely.
417+
(SimpleNamespace(), False),
418+
],
419+
)
420+
def test_is_diffusion_gemma(hf_config, expected):
421+
assert example_utils.is_diffusion_gemma(hf_config) is expected

0 commit comments

Comments
 (0)