Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 27 additions & 0 deletions examples/hf_ptq/example_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,20 @@ def is_speculative(hf_config):
)


def is_diffusion_gemma(hf_config) -> bool:
"""Check if the model architecture is DiffusionGemma.

Underscores are ignored: the family is spelled ``diffusion_gemma`` in configs
and ``DiffusionGemma`` in class names. The nested ``text_config`` is checked too,
since multi-modal wrappers keep the family name there.
"""
names = []
for cfg in (hf_config, getattr(hf_config, "text_config", None)):
names.append(getattr(cfg, "model_type", None) or "")
names.extend(getattr(cfg, "architectures", None) or [])
return any("diffusiongemma" in name.lower().replace("_", "") for name in names)
Comment thread
juhi10071998 marked this conversation as resolved.


def get_tokenizer(ckpt_path, trust_remote_code=False, **kwargs) -> PreTrainedTokenizerBase:
print(f"Initializing tokenizer from {ckpt_path}")

Expand Down Expand Up @@ -696,6 +710,19 @@ def get_model(
model_kwargs = config_kwargs.copy()
model_kwargs.setdefault("dtype", "auto")

# DiffusionGemma ties encoder/decoder weights. device_map "auto" (balanced) can split
# a tied pair across GPUs, leaving one side on the meta device and breaking generation.
# Sequential packs the model onto GPU 0 first (up to gpu_mem_percentage), keeping tied
# modules together for checkpoints that fit; larger ones can still spill and split a
# tied pair, and need an explicit single-device map. Multi-GPU only: a single-GPU split
# cannot separate a tied pair, and sequential would needlessly cap max_memory there.
if device != "cpu" and torch.cuda.device_count() > 1 and is_diffusion_gemma(hf_config):
print(
"Detected DiffusionGemma model. Using device_map='sequential'; the balanced "
Comment thread
juhi10071998 marked this conversation as resolved.
"'auto' mapping can split its tied encoder/decoder weights across GPUs."
)
use_seq_device_map = True
Comment on lines +713 to +724

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION] No escape hatch: this override is unconditional, so a multi-GPU DiffusionGemma user can no longer get device_map="auto" at all.

The forced switch changes two things for every multi-GPU DiffusionGemma load — device_map autosequential, and max_memory unset → capped at gpu_mem_percentage (0.8) per GPU. Packing onto GPU 0 first means activations and calibration buffers now compete for the same device rather than spreading across all GPUs, so a checkpoint that happened to load fine under auto (small enough that the balanced split didn't separate the tied pair) can now OOM during calibration with no way to opt back out. That's a real, if narrow, behavior change with no CLI path around it.

The print makes it visible rather than silent, and for the reported 26B-A4B-on-GB200 case the trade is clearly right — so this is non-blocking. But the surrounding per-model handling in this function (bart, t5, mxfp4) is all for cases that are broken under the alternative, whereas here auto is merely risky. Worth considering a --no_seq_device_map / tri-state flag so the override is a default rather than a hard rule, or at minimum noting in examples/llm_ptq/README.md that DiffusionGemma multi-GPU is force-sequential and how to work around an OOM (fewer visible GPUs + explicit single-device map).

Happy to defer this to a follow-up if you'd rather keep the bugfix minimal.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed on the analysis, deferring to a follow-up. The distinction you draw is the right one: bart/t5/mxfp4 special-case loads that are broken under the alternative, whereas auto here is risky rather than always fatal, so a hard override is a stronger stance than the surrounding code takes.

Keeping this PR scoped to the NVBug 6524370 fix. A tri-state flag is new CLI surface and deserves its own review; the README note belongs with it so the documented workaround matches whatever the flag ends up being. Tracking alongside the model_utils.py consolidation follow-up.

Noting the escape hatch that exists today: CUDA_VISIBLE_DEVICES=0 bypasses the override entirely, since it is gated on torch.cuda.device_count() > 1.


if use_seq_device_map:
device_map = "sequential"
# If we use sequential, set max_memory limit to ensure that the model does not occupy the full GPU
Expand Down
103 changes: 103 additions & 0 deletions tests/examples/hf_ptq/test_example_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,3 +316,106 @@ def from_pretrained(*args, **kwargs):
else:
assert "trust_remote_code" not in calls["from_config"]
assert calls["from_pretrained"]["trust_remote_code"] is True


@pytest.mark.parametrize(
("model_type", "architecture", "device_count", "expected_device_map"),
[
# DiffusionGemma ties encoder/decoder weights; "auto" can split a tied pair
# across GPUs, so multi-GPU loads must fall back to "sequential".
("diffusion_gemma", "DiffusionGemmaForConditionalGeneration", 2, "sequential"),
# Detection must also work off ``architectures`` alone, without ``model_type``.
(None, "DiffusionGemmaForConditionalGeneration", 2, "sequential"),
# Single GPU cannot split a tied pair, so it keeps the unrestricted "auto" map.
("diffusion_gemma", "DiffusionGemmaForConditionalGeneration", 1, "auto"),
# "gemma" is a substring of "diffusiongemma"; other Gemmas must not match.
("gemma3", "Gemma3ForCausalLM", 2, "auto"),
],
)
def test_get_model_device_map_for_diffusion_gemma(
monkeypatch, model_type, architecture, device_count, expected_device_map
):
calls = {}
hf_config = SimpleNamespace(
architectures=[architecture],
dtype=torch.float16,
model_type=model_type,
torch_dtype=torch.bfloat16,
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

class FakeModel:
def eval(self):
calls["eval"] = True

def parameters(self):
return iter(())

class FakeArchitecture:
@staticmethod
def _from_config(config, **kwargs):
return FakeModel()

@staticmethod
def from_pretrained(*args, **kwargs):
calls["from_pretrained"] = kwargs
return FakeModel()

monkeypatch.setattr(
example_utils.AutoConfig, "from_pretrained", lambda *args, **kwargs: hf_config
)
# Set rather than delete: ``transformers`` lazy-imports, so a deleted real class
# (e.g. Gemma3ForCausalLM) reappears on the next ``hasattr`` and the real one loads.
# raising=False: DiffusionGemma may not exist in the installed transformers.
monkeypatch.setattr(example_utils.transformers, architecture, FakeArchitecture, raising=False)
monkeypatch.setattr(example_utils, "is_nemotron_vl", lambda config: False)
monkeypatch.setattr(example_utils, "is_speculative", lambda config: False)
monkeypatch.setattr(example_utils, "init_empty_weights", lambda include_buffers: nullcontext())
monkeypatch.setattr(example_utils, "get_max_memory", lambda: {0: 1024})
monkeypatch.setattr(example_utils, "infer_auto_device_map", lambda model, max_memory: {"": 0})
monkeypatch.setattr(torch.cuda, "device_count", lambda: device_count)

example_utils.get_model("checkpoint", device="cuda", trust_remote_code=True)

assert calls["from_pretrained"]["device_map"] == expected_device_map
# Sequential caps per-GPU memory; "auto" must stay unrestricted.
if expected_device_map == "sequential":
assert calls["from_pretrained"]["max_memory"] == {0: 1024 * 0.8}
else:
assert "max_memory" not in calls["from_pretrained"]


@pytest.mark.parametrize(
("hf_config", "expected"),
[
(SimpleNamespace(model_type="diffusion_gemma", architectures=None), True),
(SimpleNamespace(model_type=None, architectures=["DiffusionGemmaForCausalLM"]), True),
(SimpleNamespace(model_type="gemma3", architectures=["Gemma3ForCausalLM"]), False),
# Multi-modal wrappers keep the family name on the nested ``text_config``.
(
SimpleNamespace(
model_type="multimodal",
architectures=["SomeWrapperForConditionalGeneration"],
text_config=SimpleNamespace(model_type="diffusion_gemma"),
),
True,
),
(
SimpleNamespace(
model_type="multimodal",
text_config=SimpleNamespace(architectures=["DiffusionGemmaForCausalLM"]),
),
True,
),
# A non-DiffusionGemma nested config must not match.
(
SimpleNamespace(
model_type="multimodal", text_config=SimpleNamespace(model_type="gemma3")
),
False,
),
# Stub configs may omit either attribute entirely.
(SimpleNamespace(), False),
],
)
def test_is_diffusion_gemma(hf_config, expected):
assert example_utils.is_diffusion_gemma(hf_config) is expected
Loading