Skip to content
151 changes: 151 additions & 0 deletions unsloth_zoo/temporary_patches/gpt_oss.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,157 @@ def patch_gpt_oss():
except Exception as e:
return raise_error("transformers.quantizers.quantizer_mxfp4.Mxfp4HfQuantizer", e)

# transformers 5.x moved MXFP4 loading from ``load_and_swizzle_mxfp4``
# (4.56.x) to the ``Mxfp4Deserialize`` WeightConverter, which is skipped
# whenever checkpoint key names already match registered parameters --
# so MXFP4 models load with raw blocks/scales still on the module and
# the swizzle step never runs. 5.x ``Mxfp4Dequantize.convert`` also
# drops the transpose that 4.x baked into ``convert_moe_packed_tensors``,
# so gate_up_proj ends up as (E, 2I, H) vs the (E, H, 2I) the stock
# forward expects. Fix both in a post-load walker.
try:
_Mxfp4Q = transformers.quantizers.quantizer_mxfp4.Mxfp4HfQuantizer
if not getattr(_Mxfp4Q, "_unsloth_post_swizzle_patched", False):
_orig_post_load = _Mxfp4Q._process_model_after_weight_loading

def _patched_post_load(self, model, **kwargs):
_orig_post_load(self, model, **kwargs)
import torch as _torch
import warnings as _warnings
dq = getattr(self.quantization_config, "dequantize", False)
if dq:
for mod in model.modules():
if type(mod).__name__ != "GptOssExperts":
continue
H = getattr(mod, "hidden_size", None)
I = getattr(mod, "intermediate_size", None)
if H is None or I is None:
continue
gup = getattr(mod, "gate_up_proj", None)
if gup is None or gup.dim() != 3:
continue
Comment on lines +157 to +159

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The code should verify that gup is actually a tensor before calling .dim(). If mod is an instance of the unsloth patched GptOssExperts and the transformers loader has not replaced the attribute, gate_up_proj will be a ParameterModule (which is an nn.Module and does not have a .dim() method), leading to an AttributeError.

Suggested change
gup = getattr(mod, "gate_up_proj", None)
if gup is None or gup.dim() != 3:
continue
gup = getattr(mod, "gate_up_proj", None)
if not isinstance(gup, _torch.Tensor) or gup.dim() != 3:
continue

_, D1, D2 = gup.shape
# gate_up_proj shape is unambiguous (2I != H);
# down_proj shape is ambiguous when I == H
# (gpt-oss-20b: 2880 / 2880).
if not (D1 == 2 * I and D2 == H):
continue
expected_wrong = {
"gate_up_proj": (2 * I, H),
"down_proj": (H, I),
}
expected_right = {
"gate_up_proj": (H, 2 * I),
"down_proj": (I, H),
}
for proj in ("gate_up_proj", "down_proj"):
p = getattr(mod, proj, None)
if p is None or p.dim() != 3:
continue
shape = tuple(p.shape[-2:])
if shape == expected_right[proj]:
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Transpose down_proj when gate_up indicates 5.x dequant layout

When dequantize=True and gate_up_proj is detected in the (E, 2I, H) layout (needs_transpose=True), down_proj still skips transposition if its last two dims equal expected_right. For gpt-oss-20b (H == I), expected_wrong["down_proj"] and expected_right["down_proj"] are both (2880, 2880), so this branch always continues and leaves down_proj in the wrong orientation. That silently changes expert outputs (wrong matrix orientation with identical shape) instead of restoring 4.x-equivalent behavior.

Useful? React with 👍 / 👎.

if shape != expected_wrong[proj]:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

When intermediate_size (I) equals hidden_size (H), which is the case for the gpt-oss-20b model (2880), the down_proj shape (H, I) is identical to the expected layout (I, H). The current check shape == expected_right[proj] will trigger in this scenario, causing the necessary transpose for down_proj to be skipped. Since the module's orientation has already been confirmed as incorrect via the gate_up_proj check on line 164, the transpose should be applied to down_proj even when the shape is ambiguous.

Suggested change
if shape == expected_right[proj]:
continue
if shape != expected_wrong[proj]:
if shape == expected_right[proj] and (proj == "gate_up_proj" or I != H):
continue
if shape != expected_wrong[proj] and shape != expected_right[proj]:

_warnings.warn(
f"[unsloth] Unexpected MXFP4-dequantize "
f"layout for {type(mod).__name__}.{proj}: "
f"got {tuple(p.shape)}. Skipping transpose."
)
continue
Comment on lines +175 to +192

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

There are two issues here:

  1. Similar to the check for gup above, p should be verified as a tensor to avoid an AttributeError if it's a ParameterModule.
  2. Bug: When hidden_size == intermediate_size (which is true for gpt-oss-20b where both are 2880), expected_right and expected_wrong for down_proj are identical (2880, 2880). The current logic at line 204 will continue and skip the transpose for down_proj, leaving the weight in the incorrect orientation even though needs_transpose was detected as True via gate_up_proj.

Refactoring the logic to check for expected_wrong first ensures the transpose is applied correctly even when shapes are ambiguous.

                        for proj in ("gate_up_proj", "down_proj"):
                            p = getattr(mod, proj, None)
                            if not isinstance(p, _torch.Tensor) or p.dim() != 3:
                                continue
                            shape = tuple(p.shape[-2:])
                            if shape == expected_wrong[proj]:
                                new_p = p.data.transpose(-2, -1).contiguous()
                                setattr(mod, proj, _torch.nn.Parameter(
                                    new_p, requires_grad=p.requires_grad,
                                ))
                            elif shape != expected_right[proj]:
                                _warnings.warn(
                                    f"[unsloth] Unexpected MXFP4-dequantize "
                                    f"layout for {type(mod).__name__}.{proj}: "
                                    f"got {tuple(p.shape)}, expected "
                                    f"(..., {expected_wrong[proj][0]}, "
                                    f"{expected_wrong[proj][1]}) or (..., "
                                    f"{expected_right[proj][0]}, "
                                    f"{expected_right[proj][1]}). Skipping "
                                    f"transpose; forward may fail. This "
                                    f"usually means your transformers "
                                    f"version changed the dequantize layout."
                                )

new_p = p.data.transpose(-2, -1).contiguous()
setattr(mod, proj, _torch.nn.Parameter(
new_p, requires_grad=p.requires_grad,
))
return

import transformers.integrations.mxfp4 as _mx_mod
swizzle_fn = getattr(_mx_mod, "swizzle_mxfp4_convertops", None)
if swizzle_fn is None:
for mod in model.modules():
if type(mod).__name__ != "Mxfp4GptOssExperts":
continue
if "_gate_up_proj" in mod.__dict__:
continue
if getattr(mod, "gate_up_proj_blocks", None) is not None:
raise RuntimeError(
"[unsloth] MXFP4 model has raw blocks/scales "
"post-load but swizzle_mxfp4_convertops is "
"not available; pass Mxfp4Config(dequantize="
"True) to fall back to bf16."
)
return
try:
import triton_kernels as _tk
except Exception as _tk_err:
for mod in model.modules():
if type(mod).__name__ != "Mxfp4GptOssExperts":
continue
if "_gate_up_proj" in mod.__dict__:
continue
if getattr(mod, "gate_up_proj_blocks", None) is not None:
raise RuntimeError(
"[unsloth] Native MXFP4 requires "
"triton_kernels; install it or pass "
"Mxfp4Config(dequantize=True) for bf16."
) from _tk_err
return
for mod in model.modules():
if type(mod).__name__ != "Mxfp4GptOssExperts":
continue
if "_gate_up_proj" in mod.__dict__:
continue
blocks = getattr(mod, "gate_up_proj_blocks", None)
scales = getattr(mod, "gate_up_proj_scales", None)
if blocks is None or scales is None:
continue
if blocks.device.type == "meta":
continue
for proj in ("gate_up_proj", "down_proj"):
b = getattr(mod, f"{proj}_blocks", None)
s = getattr(mod, f"{proj}_scales", None)
if b is None or s is None:
continue
try:
swizzle_fn(b.data, s.data, mod, proj, b.device, _tk)
except Exception as _sw_err:
raise RuntimeError(
f"[unsloth] MXFP4 swizzle failed on "
f"{type(mod).__name__}.{proj}; likely "
f"triton_kernels / transformers drift."
) from _sw_err
if f"{proj}_blocks" in mod._parameters:
del mod._parameters[f"{proj}_blocks"]
if f"{proj}_scales" in mod._parameters:
del mod._parameters[f"{proj}_scales"]

_Mxfp4Q._process_model_after_weight_loading = _patched_post_load
_Mxfp4Q._unsloth_post_swizzle_patched = True

# triton_kernels MXFP4 matmul is Hopper-only; Ampere/Ada/Blackwell
# abort with "Only Hopper swizzling is supported". Force dequantize
# on any non-Hopper (or CPU-only) environment before load.
if not getattr(_Mxfp4Q, "_unsloth_cpu_gate_patched", False):
_orig_before_load = _Mxfp4Q._process_model_before_weight_loading

def _patched_before_load(self, model, **kwargs):
try:
import torch as _torch
if (
not _torch.cuda.is_available()
and not getattr(self.quantization_config, "dequantize", False)
):
self.quantization_config.dequantize = True

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Detect non-Hopper GPUs before keeping MXFP4 quantized

The new _patched_before_load guard only flips dequantize when CUDA is unavailable, so any CUDA host with a non-Hopper GPU (for example A100/B200) still keeps dequantize=False when users request native MXFP4. In that environment the model proceeds down the quantized path and later hits the Hopper-only kernel failure (Only Hopper swizzling is supported), so this check does not enforce the fallback described in the surrounding comments.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The implementation of _patched_before_load does not include the Hopper-specific hardware check described in the pull request summary. Currently, it only forces dequantization when CUDA is completely unavailable. To prevent crashes on non-Hopper GPUs (like T4, A100, or B200) where native MXFP4 swizzling is unsupported, the logic should inspect the compute capability of all visible CUDA devices and force dequantization if any non-Hopper device is found.

Suggested change
if (
not _torch.cuda.is_available()
and not getattr(self.quantization_config, "dequantize", False)
):
self.quantization_config.dequantize = True
force_dq = not _torch.cuda.is_available()
if not force_dq:
for i in range(_torch.cuda.device_count()):
if _torch.cuda.get_device_capability(i) != (9, 0):
force_dq = True
break
if force_dq and not getattr(self.quantization_config, "dequantize", False):
self.quantization_config.dequantize = True

except Exception:
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
pass
return _orig_before_load(self, model, **kwargs)

_Mxfp4Q._process_model_before_weight_loading = _patched_before_load
_Mxfp4Q._unsloth_cpu_gate_patched = True
except Exception as e:
return raise_error(
"transformers.quantizers.quantizer_mxfp4.Mxfp4HfQuantizer post-load swizzle patch", e,
)

if HAS_TRITON_KERNELS:
# Only override is_kernels_available when triton_kernels IS available
try:
Expand Down
Loading