Skip to content
13 changes: 9 additions & 4 deletions src/transformers/integrations/mxfp4.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,16 +289,21 @@ def mlp_forward(self, hidden_states):
else:
routing = triton_kernels_hub.routing.routing

routing = routing
batch_size = hidden_states.shape[0]
hidden_states = hidden_states.reshape(-1, self.router.hidden_dim)
is_3d = hidden_states.ndim == 3
if is_3d:
batch_size, seq_len, _ = hidden_states.shape
hidden_states = hidden_states.reshape(-1, self.router.hidden_dim)

Comment on lines +292 to +296

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

same here, I am not sure I understand the fix

@PrathmeshAdsod PrathmeshAdsod Aug 26, 2025 •

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.

Let me clarify the differences between the original code and my fix.
This is considering my understanding of current code and Key-Value cache system for second and subsequent terns.

How the Original Code Handled the First Turn (The Successful Path)
The original code focused only on receiving a 3D tensor. It followed a strict pipeline that worked perfectly for this situation.

Here is the flow of the original code when it received the 3D tensor on the first turn:

Step 1: Receive 3D Tensor
The mlp_forward function gets the 3D hidden_states tensor, for example, (1, 50, 8192).

Step 2: Unpack Shape
The code runs batch_size, seq_len, _ = hidden_states.shape.
For a 3D tensor, this works well. It correctly identifies batch_size=1 and seq_len=50.

Step 3: Flatten Tensor
The code then executes hidden_states = hidden_states.reshape(...).
This flattens the 3D tensor into a 2D tensor, such as (50, 8192). This also works perfectly.

Step 4: Process with Experts
The new 2D tensor is passed to the MlpExperts layer. It processes the data and returns a 2D result.

Step 5: Reshape Output
The code takes the 2D result and uses the batch_size and seq_len from Step 2 to reshape it back into a 3D tensor. This step is successful too.
Conclusion for the first turn: The original code functioned without issues because its strict, step-by-step process matched the 3D input it received.

Why the Original Code Failed on the Second Turn
The crash may happened because the rigid approach could not handle the 2D tensor from the second turn.

1 seq_len KV cache
On the second turn, the 2D tensor (1, 8192) came in.
The code attempted to run its first operation, batch_size, seq_len, _ = hidden_states.shape.
This caused a error since it tried to unpack a shape with only two values into three variables. The program crashed.

How My Fix Corrects This
My fix keeps the successful logic from the first turn intact. It simply wraps the parts safe for 3D tensors inside the if is_3d: check.

For the first turn, is_3d is True, so the code runs the same successful steps for unpacking and reshaping as the original code did.
For the second turn, is_3d is False, so the code skips the unsafe unpacking and reshaping steps, avoiding the crash.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

hmmm you are saying that it crashed when trying to unpack which doesn't any sense with the original issue.

This caused a error since it tried to unpack a shape with only two values into three variables. The program crashed.

router_logits = nn.functional.linear(hidden_states, self.router.weight, self.router.bias)

with torch.cuda.device(router_logits.device):
routing_data, gather_idx, scatter_idx = routing(router_logits, self.router.top_k)

routed_out = self.experts(hidden_states, routing_data, gather_idx, scatter_idx)
routed_out = routed_out.reshape(batch_size, -1, self.router.hidden_dim)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

btw one thing I am not getting is that regardless, this line is equivalent (if 2d, you should still have batch size 1.

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.

That line kept inside if statement that only runs when the original input was 3D. For the second turn of a chat, the input is 2D, so the if condition is false and that reshape line is completely skipped. The code correctly returns a 2D output to match the 2D input. Otherwise if not in if statement then can cause fake dimension issues. 2D is for second and subsequent terns


if is_3d:
routed_out = routed_out.reshape(batch_size, seq_len, self.router.hidden_dim)

return routed_out, router_logits


Expand Down
107 changes: 105 additions & 2 deletions tests/quantization/mxfp4/test_mxfp4.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,13 @@


if is_torch_available():
from types import SimpleNamespace

import torch

from transformers.integrations.mxfp4 import mlp_forward
from transformers.testing_utils import torch_device


class Mxfp4ConfigTest(unittest.TestCase):
def test_basic_config_creation(self):
Expand Down Expand Up @@ -194,7 +199,7 @@ def test_quantizer_validation_missing_triton(self):
"""Test quantizer validation when triton is not available"""
with (
patch("transformers.quantizers.quantizer_mxfp4.is_triton_available", return_value=False),
patch("transformers.quantizers.quantizer_mxfp4.is_kernels_availalble", return_value=False),
patch("transformers.quantizers.quantizer_mxfp4.is_kernels_available", return_value=False),
):
from transformers.quantizers.quantizer_mxfp4 import Mxfp4HfQuantizer

Expand All @@ -208,7 +213,7 @@ def test_quantizer_validation_missing_triton_pre_quantized_no_dequantize(self):
"""Test quantizer validation when triton is not available but model is pre-quantized and dequantize is False"""
with (
patch("transformers.quantizers.quantizer_mxfp4.is_triton_available", return_value=False),
patch("transformers.quantizers.quantizer_mxfp4.is_kernels_availalble", return_value=False),
patch("transformers.quantizers.quantizer_mxfp4.is_kernels_available", return_value=False),
):
from transformers.quantizers.quantizer_mxfp4 import Mxfp4HfQuantizer

Expand Down Expand Up @@ -365,6 +370,63 @@ def test_quantize_to_mxfp4(self):
self.assertIsNotNone(flex_data)
self.assertIsNotNone(mx_ctx)

@require_torch
@patch("torch.cuda.device")
@patch("transformers.integrations.mxfp4.triton_kernels_hub", create=True)
def test_mlp_forward_dimensionality(self, mock_triton_hub, mock_cuda_device):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

sorry I should have specify but what we want for a test is a multiturn example that waas failing before the PR and is now fixed 😉

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.

Hi @ArthurZucker , I added the test case for multi-turn simulation. run_tests are passed, can you check our case is PASSED or SKIPPED. I am unable to see it.

"""
Tests that the `mlp_forward` function correctly handles 2D and 3D tensors,
preserving the input dimensionality in the output. This is a regression
test for an issue seen in multi-turn chat scenarios.
"""
# 1. Setup
hidden_dim = 32
num_experts = 4
top_k = 1

# Mock the routing function from the triton kernel
mock_routing_data = SimpleNamespace() # Dummy object
mock_gather_idx = torch.zeros(10, dtype=torch.int32, device=torch_device)
mock_scatter_idx = torch.zeros(10, dtype=torch.int32, device=torch_device)
mock_triton_hub.routing.routing.return_value = (mock_routing_data, mock_gather_idx, mock_scatter_idx)

# Mock the 'self' object that mlp_forward is bound to
mock_self = SimpleNamespace()
mock_self.router = SimpleNamespace(
hidden_dim=hidden_dim,
weight=torch.randn((num_experts * top_k, hidden_dim), device=torch_device),
bias=torch.randn(num_experts * top_k, device=torch_device),
top_k=top_k,
)

# The experts function should return a tensor of the same shape as its input
def mock_experts_forward(hidden_states, *args, **kwargs):
return hidden_states

mock_self.experts = mock_experts_forward

# 2. Test with 2D input
hidden_states_2d = torch.randn(10, hidden_dim, device=torch_device)
output_2d, _ = mlp_forward(mock_self, hidden_states_2d)

self.assertEqual(len(output_2d.shape), 2, "Output should be 2D for 2D input")
self.assertEqual(hidden_states_2d.shape, output_2d.shape, "Shape should be preserved for 2D input")

# 3. Test with 3D input
hidden_states_3d = torch.randn(2, 5, hidden_dim, device=torch_device)
# The mock routing indices need to match the flattened input size
num_tokens = hidden_states_3d.shape[0] * hidden_states_3d.shape[1]
mock_triton_hub.routing.routing.return_value = (
mock_routing_data,
torch.zeros(num_tokens, dtype=torch.int32, device=torch_device),
torch.zeros(num_tokens, dtype=torch.int32, device=torch_device),
)

output_3d, _ = mlp_forward(mock_self, hidden_states_3d)

self.assertEqual(len(output_3d.shape), 3, "Output should be 3D for 3D input")
self.assertEqual(hidden_states_3d.shape, output_3d.shape, "Shape should be preserved for 3D input")


@require_torch
@require_torch_large_gpu
Expand Down Expand Up @@ -479,3 +541,44 @@ def test_memory_footprint_comparison(self):
quantized_mem = quantized_model.get_memory_footprint()
dequantized_mem = dequantized_model.get_memory_footprint()
self.assertLess(quantized_mem, dequantized_mem)

def test_gpt_oss_multi_turn_generation(self):
"""
An integration test to ensure multi-turn chat works with GPT-OSS, specifically testing the fix
for the tensor shape mismatch in the MXFP4 MLP layer. This test was failing before the fix
when Mxfp4Config(dequantize=False) i.e using this quantized version
"""

# we want dequantize=False to test for multi-turn
quantization_config = Mxfp4Config(dequantize=False)
model = GptOssForCausalLM.from_pretrained(
self.model_name,
quantization_config=quantization_config,
torch_dtype=torch.bfloat16,
device_map="auto",
)
tokenizer = AutoTokenizer.from_pretrained(self.model_name)

# Turn 1: Initial question
prompt_1 = "Who directed the movie Inception?"
inputs_1 = tokenizer(prompt_1, return_tensors="pt").to(model.device)

# This first generation uses a 3D tensor internally
output_1_ids = model.generate(**inputs_1, max_new_tokens=20)
response_1 = tokenizer.decode(output_1_ids[0], skip_special_tokens=True)

# Turn 2: Follow-up question
prompt_2 = response_1 + "\nAnd what other movies has he directed?"
inputs_2 = tokenizer(prompt_2, return_tensors="pt").to(model.device)

# This is the step that would previously crash with the `num_stages` assertion error.
try:
output_2_ids = model.generate(**inputs_2, max_new_tokens=50)
response_2 = tokenizer.decode(output_2_ids[0], skip_special_tokens=True)
except Exception as e:
self.fail(f"Multi-turn generation failed with MXFP4. Error: {e}")

# Assert that we got a coherent, non-empty response
# It will tell that our multi-turn has success
self.assertIn("Inception", response_1)
self.assertTrue(len(response_2) > len(prompt_2))