Skip to content

[xpu] Route per-block FP8 GEMM through torch._scaled_mm on XPU#4548

Open
hoshibara wants to merge 6 commits into
pytorch:mainfrom
hoshibara:xingyuan-per-block-scaled-mm-xpu
Open

[xpu] Route per-block FP8 GEMM through torch._scaled_mm on XPU#4548
hoshibara wants to merge 6 commits into
pytorch:mainfrom
hoshibara:xingyuan-per-block-scaled-mm-xpu

Conversation

@hoshibara

Copy link
Copy Markdown

On Intel BMG XPU, the original blockwise_fp8_gemm triton kernel was an order of magnitude slower than per-row scaled_mm. XPU's oneDNN _scaled_mm natively supports mixed-mode (act=1x128, weight=128x128) where b is interpreted as [K, N] and scale_b as [K/128, N/128].

For the XPU path only, call addmm_float8_unwrapped_inference with:

  • a = inpt_data [M, K] row-major
  • scale_a = input_tensor.scale.squeeze(-1) [M, K/128]
  • b = weight_tensor.qdata [K, N] column-major view
  • scale_b = weight_tensor.scale [K/128, N/128]

The existing blockwise_fp8_gemm triton path is preserved as the default for non-XPU devices.

Verified on BMG Arc B580: per-block eager GEMM now 0.61x of per-row at batch=1 and 1.26x at batch=1024; shape-correctness max_err < 0.08 across 6 configs.

@pytorch-bot

pytorch-bot Bot commented Jul 2, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/ao/4548

Note: Links to docs will display an error until the docs builds have been completed.

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Jul 2, 2026
On Intel BMG XPU, the original blockwise_fp8_gemm triton kernel was an
order of magnitude slower than per-row scaled_mm. XPU's oneDNN _scaled_mm
natively supports mixed-mode (act=1x128, weight=128x128) where b is
interpreted as [K, N] and scale_b as [K/128, N/128].

For the XPU path only, call addmm_float8_unwrapped_inference with:
- a = inpt_data  [M, K] row-major
- scale_a = input_tensor.scale.squeeze(-1)  [M, K/128]
- b = weight_tensor.qdata  [K, N] column-major view
- scale_b = weight_tensor.scale  [K/128, N/128]

The existing blockwise_fp8_gemm triton path is preserved as the default
for non-XPU devices.

Verified on BMG Arc B580: per-block eager GEMM now 0.61x of per-row at batch=1
and 1.26x at batch=1024; shape-correctness max_err < 0.08 across 6 configs.

Co-Authored-Extended: Claude <noreply@anthropic.com>
@hoshibara
hoshibara force-pushed the xingyuan-per-block-scaled-mm-xpu branch from 5177cb9 to fe6c37f Compare July 2, 2026 09:47
hoshibara added 2 commits July 7, 2026 11:09
The XPU path previously passed raw input_tensor.scale/weight_tensor.qdata
directly, bypassing preprocess_scale and preprocess_data. This meant
pad_inner_dim and contiguity fixes were skipped, which is only safe
when K is 128-aligned.

Change to reuse the same preprocessed inpt_data/input_scale/w_data/w_scale
that the CUDA triton path and per-row path already consume.
The only intentional difference is b passed without .t(): oneDNN
expects column-major weights (already arranged by preprocess_data),
while blockwise_fp8_gemm expects row-major.
…wise path

Use only inpt_data/input_scale/w_data/w_scale in the XPU _is_128_128_scaled
path. Replace input_tensor.scale.shape[-1] -> input_scale.shape[-1] and
input_tensor.dtype -> input_scale.dtype.
@xiaowangintel xiaowangintel added module: inference quantize_ api inference flow ciflow/xpu label used to trigger xpu CI jobs labels Jul 7, 2026
@pytorch-bot

pytorch-bot Bot commented Jul 7, 2026

Copy link
Copy Markdown

Workflows were awaiting approval. CI has now been triggered for the ciflow labels on this PR.

@xiaowangintel

Copy link
Copy Markdown
Collaborator

@claude pls follow this skills #3823 (comment) to review this pr.

@claude

claude Bot commented Jul 7, 2026

Copy link
Copy Markdown

Claude finished @xiaowangintel's task in 3m 19s —— View job


PR Review — Route per-block FP8 GEMM through torch._scaled_mm on XPU

⚠️ I was unable to fetch the linked skill at pytorch/ao#3823 (comment) — outbound network / gh api calls are blocked in this sandboxed run. I've applied a standard thorough correctness + style review instead. If that comment contains a specific review checklist you want followed, please paste it here and I'll re-run against it.


Summary

The change routes the 128x128 (weight) × 1x128 (activation) blockwise FP8 path through addmm_float8_unwrapped_inference (→ torch._scaled_mm) on XPU, while keeping the Triton blockwise_fp8_gemm as the default for other devices. The refactor to use the preprocessed inpt_data / input_scale / w_data / w_scale locals (instead of the raw input_tensor / weight_tensor fields) is a good cleanup and consistent with the non-XPU branch below it. Nice perf win on BMG.

Most of my feedback is on one likely bug and a few robustness gaps.


🔴 Likely bug: output_dtype=input_scale.dtype is wrong

float8_tensor.py:443

output_dtype=input_scale.dtype,

input_scale is the quantization scale, which is always produced as torch.float32 — see _choose_scale_float8 in quant_primitives.py:2282 (return scale.to(dtype=torch.float32)). So this forces the GEMM output to float32 regardless of the model's compute dtype.

The intent is almost certainly the activation's original high-precision dtype, which is what the non-XPU/rowwise branches use: output_dtype=input_tensor.dtype (float8_tensor.py:467). input_tensor.dtype is the stored hp_dtype (e.g. bfloat16), set at float8_tensor.py:252.

As written, an fp8-quantized bf16 model would return fp32 activations from this path only, producing a dtype mismatch vs. the rest of the network and inflating memory/bandwidth. Your correctness test likely still passed because it compares numerics (fp32 is a superset of bf16 values) rather than asserting out.dtype.

Suggested fix:

output_dtype=input_tensor.dtype,

Fix this →


🟡 input_scale.reshape(inpt_data.shape[0], -1) — verify contiguity & M mismatch

float8_tensor.py:440

input_scale.reshape(inpt_data.shape[0], -1)

Two things worth confirming:

  1. Reshape vs. preprocess_scale output. preprocess_scale (inference.py:68) has already unsqueeze(-1)'d the scale to (M*K/128, 1) and may have collapsed higher dims. Reshaping (M*K/128, 1)(M, K/128) is only valid if the flattened layout is row-major contiguous in the exact (M, K/128) order. This holds for a 2D activation, but for a 3D+ activation (inpt_data = input_tensor.qdata.reshape(-1, ...) at line 416 flattens leading dims into M), please confirm the scale's element order still lines up after preprocess_scale — a silent transpose here would give wrong-but-plausible results. A quick test with a 3D activation (batch, seq, hidden) would de-risk this.

  2. Given (1), it may be cleaner to compute the XPU scale directly from input_tensor.scale before preprocess_scale mangles it, rather than reshaping the preprocessed form back. That would make the intended (M, K/128) shape explicit rather than reverse-engineered.


🟡 w_data / w_scale orientation — no transpose, unlike the Triton path

The Triton branch passes w_data.t() and w_scale.t() (float8_tensor.py:455-456), whereas the XPU branch passes w_data / w_scale untransposed. The PR description says oneDNN interprets b as [K, N] with scale_b as [K/128, N/128], so this is presumably intentional and correct for the XPU kernel — but since it's a subtle asymmetry, a one-line comment stating "XPU oneDNN expects b as [K, N] (no transpose)" would help future readers avoid "fixing" it to match the Triton path.


🟢 Minor

  • preprocess_data is still called for the XPU path (float8_tensor.py:431). That function may .t().contiguous().t() w_data into column-major (inference.py:63-64). Confirm the resulting w_data stride is what oneDNN expects — the PR description says "column-major view", which matches, but worth a sanity check that pad_inner_dim (if ever set) doesn't interfere.
  • Comment wording (float8_tensor.py:436-437): "_scaled_mm rejects preprocess_scale's (M*K/128, 1)" — good context. Consider noting which backend rejects it (XPU/oneDNN) since CUDA reaches the same preprocess_scale.
  • No XPU-specific test. The perf/correctness numbers in the description are great, but there's no automated test guarding this path (understandable given XPU CI gating). A @unittest.skipIf(not xpu) test asserting output shape and dtype would have caught the output_dtype issue above.

Verdict

The overall approach is sound and the refactor is clean. The one blocking item is output_dtype=input_scale.dtypeinput_tensor.dtype; the reshape contiguity for 3D inputs is the next thing I'd verify before landing. The draft status and pending XPU CI are appropriate.
· branch xingyuan-per-block-scaled-mm-xpu

Two related fixes for the XPU blockwise path (PerBlock[1,128] act,
PerBlock[128,128] wt) routed through torch._scaled_mm:

1. input_scale reshape: preprocess_scale flattens the 3D scale
   (1, seq, K/128) into (M*K/128, 1), and .reshape(-1, scale.shape[-1])
   was a no-op because shape[-1]==1. Use inpt_data.shape[0] to
   reconstruct the correct (M, K/128) shape.

2. output_dtype: the refactored code passed input_scale.dtype (float32
   after preprocess_scale) to _scaled_mm, producing float32 output.
   Restore input_tensor.dtype (bfloat16) to match the non-blockwise
   path and the original fe6c37f behavior.
@hoshibara
hoshibara force-pushed the xingyuan-per-block-scaled-mm-xpu branch from aa3b093 to 668816b Compare July 7, 2026 09:18
@hoshibara hoshibara changed the title [Draft][xpu] Route per-block FP8 GEMM through torch._scaled_mm on XPU [xpu] Route per-block FP8 GEMM through torch._scaled_mm on XPU Jul 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ciflow/xpu label used to trigger xpu CI jobs CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. module: inference quantize_ api inference flow

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants