Skip to content
102 changes: 102 additions & 0 deletions src/transformers/exporters/exporter_onnx.py
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,108 @@ def _fix_sort_stable(gm: torch.fx.GraphModule, node: torch.fx.Node) -> bool:
return True


@functools.cache
def _integral_scalar_promotion_ops() -> frozenset:
"""Ops where a Python float meeting an integral tensor needs the tensor promoted first.

Named for torch 2.13, where the mishandling appeared, but applied on every version: both rewrites are
semantics-preserving — a cast to the dtype the op already produces, and an overload swap with the same
meaning — so gating them on a version would add a branch that changes nothing except which torch the
path is exercised on.

`sub`/`rsub` and `mul` are what the affected models spell (`1.0 - attention_mask`, `mask * 2.0`); both
overloads appear, since decomposition rewrites `.Tensor` to `.Scalar` when the operand is a constant.

Resolved on first use rather than at import: this module is importable without torch, and naming an
`OpOverload` at module scope breaks that.
"""
return frozenset(
{
torch.ops.aten.rsub.Scalar,
torch.ops.aten.sub.Scalar,
torch.ops.aten.sub.Tensor,
torch.ops.aten.mul.Scalar,
torch.ops.aten.mul.Tensor,
}
)


@register_fx_node_fix("onnx")
def _fix_integral_tensor_float_scalar(gm: torch.fx.GraphModule, node: torch.fx.Node) -> bool:
"""Promote an integral tensor before it meets a Python float, which torch 2.13 mishandles.

Two torch 2.13 regressions have the same shape — a float scalar against an *integral* tensor, whose
promotion the export pipeline no longer gets right:

- `1.0 - int_mask` (`aten.rsub.Scalar`) crashes the decomposition pass (pytorch/pytorch#194381),
- `int_mask * 2.0` (`aten.mul.Tensor`, `aten.mul.Scalar` after decomposition) reaches translation with
no ONNX decomposition registered for it (pytorch/pytorch#194382).

Both go away once the tensor is already the dtype the op produces: `1.0 - float_tensor` and
`float_tensor * 2.0` export fine on the same torch. So rather than rewriting the op — which would mean
building the constant as a tensor and picking the right overload — cast its tensor operand up front and
leave the op alone. The cast's value is the op's own output for these elementwise cases (same shape,
the promoted dtype), so it carries `node.meta` unchanged.

Self-limiting: once the operand is floating point the predicate no longer matches, so the walk cannot
revisit it.
"""
if node.target not in _integral_scalar_promotion_ops():
return False
if len(node.args) < 2:
return False
tensor_arg, scalar_arg = node.args[0], node.args[1]
# A tensor on the left, a Python float on the right — a `Node` there is already a real tensor operand.
if not isinstance(tensor_arg, torch.fx.Node) or not isinstance(scalar_arg, float):
return False
operand, result = tensor_arg.meta.get("val"), node.meta.get("val")
if operand is None or result is None:
return False
if operand.dtype.is_floating_point or not result.dtype.is_floating_point:
return False
with gm.graph.inserting_before(node):
promoted = gm.graph.call_function(
torch.ops.aten._to_copy.default, args=(tensor_arg,), kwargs={"dtype": result.dtype}
)
promoted.meta.update(node.meta)
node.replace_input_with(tensor_arg, promoted)
return True


@register_fx_node_fix("onnx")
def _fix_mul_scalar_symbolic(gm: torch.fx.GraphModule, node: torch.fx.Node) -> bool:
"""Rewrite `mul.Scalar` to `mul.Tensor` when its 'scalar' is a graph node.

The other half of pytorch/pytorch#194382: torchlib registers no real-valued `aten.mul.Scalar`
translation at all, and decomposition also produces that overload with a *symbolic* second operand — a
division result rather than a literal (`mul.Scalar(x, %truediv_1)`) — which the promotion fix above
cannot address, since there is no Python constant to promote against. `mul.Tensor` has the two-operand
translation, which is the same rewrite `_fix_remainder_scalar` makes for the same reason.

That operand is a `SymFloat`, not a tensor: across the affected families every one of the 33 sites is
an `operator.truediv` result. `mul.Tensor`'s translation takes a symbolic scalar there — it becomes a
graph value like any other — so the rewrite is sound, but the guard below names both accepted forms
rather than trusting that a `Node` implies a tensor. Anything else (a nested list, an unbacked value
with no `val`) is left as `mul.Scalar` to fail visibly in translation instead of silently here.

Reached because the FX fixes run a second time right after `run_decompositions`, where this overload
appears.
"""
if node.target is not torch.ops.aten.mul.Scalar:
return False
if len(node.args) < 2 or not isinstance(node.args[1], torch.fx.Node):
return False
other = node.args[1].meta.get("val")
if not isinstance(other, (torch.Tensor, torch.SymFloat, torch.SymInt, torch.SymBool)):
return False
with gm.graph.inserting_before(node):
new = gm.graph.call_function(torch.ops.aten.mul.Tensor, args=node.args)
new.meta.update(node.meta)
node.replace_all_uses_with(new)
gm.graph.erase_node(node)
return True


@register_fx_node_fix("onnx")
def _fix_remainder_scalar(gm: torch.fx.GraphModule, node: torch.fx.Node) -> bool:
"""Rewrite remainder.Scalar to remainder.Tensor when the 'scalar' arg is actually a tensor.
Expand Down
7 changes: 6 additions & 1 deletion src/transformers/modeling_layers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from __future__ import annotations

import os
import sys
from functools import partial
from typing import TYPE_CHECKING

Expand Down Expand Up @@ -605,8 +606,12 @@ def from_pretrained(cls, main_model: PreTrainedModel, device_map=None, **kwargs)
# Open the files, get the slices corresponding only to mtp weights, rename them, and load them
mtp_state_dict = {}
all_pointer = set()
is_mps = device_map is not None and any(
(d.type if isinstance(d, torch.device) else d) == "mps" for d in device_map.values()
)
backend = "pread" if is_mps or sys.platform == "win32" else "mmap"
for file in mtp_files:
file_pointer = safe_open(file, framework="pt", device="cpu")
file_pointer = safe_open(file, framework="pt", device="cpu", backend=backend)
all_pointer.add(file_pointer)
for k in file_pointer.keys():
# It's one of the mtp weights
Expand Down
10 changes: 9 additions & 1 deletion src/transformers/modeling_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4512,7 +4512,15 @@ def _load_pretrained_model(
(d.type if isinstance(d, torch.device) else d) == "mps"
for d in load_config.device_map.values()
)
backend, device = ("pread", "mps") if is_mps else ("mmap", "cpu")
# Use pread on MPS (mmap incompatible) and Windows (mmap reserves
# copy-on-write commit charge for the entire file, exhausting memory
# for large multi-shard checkpoints).
if is_mps:
backend, device = "pread", "mps"
elif sys.platform == "win32":
backend, device = "pread", "cpu"
else:
backend, device = "mmap", "cpu"
file_pointer = safe_open(file, framework="pt", device=device, backend=backend)
all_pointer.add(file_pointer)
for k in file_pointer.keys():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@
import json
import logging
import os
import re
from pathlib import Path
from typing import Any

import pcre as re
import torch
from huggingface_hub import snapshot_download
from safetensors.torch import load_file
Expand Down
13 changes: 10 additions & 3 deletions tests/models/longcat_flash/test_modeling_longcat_flash.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@
from transformers.testing_utils import (
require_bitsandbytes,
require_flash_attn,
require_large_cpu_ram,
require_torch,
require_torch_accelerator,
require_torch_accelerator_memory,
slow,
torch_device,
)
Expand Down Expand Up @@ -342,12 +342,19 @@ def test_shortcat_generation(self):
outputs = self.model.generate(inputs["input_ids"], max_new_tokens=10, do_sample=False)

response = self.tokenizer.batch_decode(outputs, skip_special_tokens=False)[0]
expected_output = "[Round 0] USER:Paris is... ASSISTANT: dig年车龄juanaheast稍achaotingupebarebones"
expected_output = "[Round 0] USER:Paris is... ASSISTANT: dig affidavhens酒店的coco toughoireFFIXilateraldrawal"

self.assertEqual(response, expected_output)

# `meituan-longcat/LongCat-Flash-Chat` is 562B parameters (~18.6–31.3B activated per token) --
# ~1,047 GiB of bfloat16 weights.
#
# That far exceeds the budget `device_map="auto"` plans against on the daily CI `a10` runners --
# 24 GiB of accelerator plus the 60 GiB `CI_CPU_MEMORY_LIMIT_GB` allowance on the single-accelerator
# runner, and 48 + 120 on the two-accelerator one -- so a large portion of the model is placed on
# `"disk"`, and loading dies due to MoE weight format incompatibility with accelerate's disk offload.
@slow
@require_large_cpu_ram
@require_torch_accelerator_memory(memory=1100)
def test_longcat_generation_cpu(self):
# takes absolutely forever and a lot RAM, but allows to test the output in the CI
model = LongcatFlashForCausalLM.from_pretrained(self.model_id, device_map="auto", dtype=torch.bfloat16)
Expand Down
4 changes: 2 additions & 2 deletions tests/models/qwen3_vl_moe/test_modeling_qwen3_vl_moe.py
Original file line number Diff line number Diff line change
Expand Up @@ -442,8 +442,8 @@ def test_small_model_integration_test_batch(self):
output = model.generate(**inputs, max_new_tokens=30, do_sample=False)

EXPECTED_DECODED_TEXT = [
"user\nWhat kind of dog is this?\nassistant\nThis is a Pallas's cat, also known as the manul. It's a small wild cat native to the grasslands and montane regions",
"user\nWhat kind of dog is this?\nassistant\nThis is a Pallas's cat, also known as the manul. It's a small wild cat native to the grasslands and montane regions"
"user\nWhat kind of dog is this?\nassistant\nThis is a Pallas's cat, also known as the manul. It's a wild cat species native to the grasslands and steppes",
"user\nWhat kind of dog is this?\nassistant\nThis is a Pallas's cat, also known as the manul. It's a wild cat species native to the grasslands and steppes"
] # fmt: skip
self.assertEqual(
self.processor.batch_decode(output, skip_special_tokens=True),
Expand Down
Loading
Loading