Repository navigation
feat(musa): add native deterministic gemm kernel #395
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,179 @@ | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| // Copyright (c) 2026 RL-Kernel Contributors | ||
|
|
||
| #include <musa_runtime.h> | ||
| #include <c10/core/DeviceGuard.h> | ||
| #include <torch/extension.h> | ||
| #include <torch_musa/csrc/aten/musa/Exceptions.h> | ||
| #include <torch_musa/csrc/aten/musa/MUSAContext.h> | ||
|
|
||
| namespace { | ||
|
|
||
| constexpr int kBlockSize = 256; | ||
|
|
||
| template < | ||
| typename input_t, | ||
| typename output_t, | ||
| bool TransposeA, | ||
| bool TransposeB, | ||
| bool TransposeOutput> | ||
| __global__ void det_gemm_kernel( | ||
| const input_t* a, | ||
| const input_t* b, | ||
| output_t* c, | ||
| int m, | ||
| int n, | ||
| int k) { | ||
| const int index = blockIdx.x * blockDim.x + threadIdx.x; | ||
| if (index >= m * n) { | ||
| return; | ||
| } | ||
| const int row = index / n; | ||
| const int column = index % n; | ||
| float accumulator = 0.0f; | ||
| for (int inner = 0; inner < k; ++inner) { | ||
| const int a_index = TransposeA ? inner * m + row : row * k + inner; | ||
| const int b_index = TransposeB ? column * k + inner : inner * n + column; | ||
| accumulator += | ||
| static_cast<float>(a[a_index]) * static_cast<float>(b[b_index]); | ||
| } | ||
| const int output_index = TransposeOutput ? column * m + row : index; | ||
| c[output_index] = static_cast<output_t>(accumulator); | ||
| } | ||
|
|
||
| template <typename input_t, typename output_t> | ||
| void launch( | ||
| torch::Tensor a, | ||
| torch::Tensor b, | ||
| torch::Tensor output, | ||
| int m, | ||
| int n, | ||
| int k, | ||
| bool transpose_a, | ||
| bool transpose_b, | ||
| bool transpose_output) { | ||
| const int blocks = (m * n + kBlockSize - 1) / kBlockSize; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. m, n, k and the kernel indexes are int, so m * n and row * k can overflow. Signed overflow is undefined behavior. torch::empty still receives {m, n} separately, so the allocation can succeed while the grid and kernel bounds use the bad product. Widen to int64_t before multiplying, or reject oversized shapes before launch. |
||
| auto stream = at::musa::getCurrentMUSAStream(); | ||
| if (transpose_a) { | ||
| if (transpose_output) { | ||
| det_gemm_kernel<input_t, output_t, true, false, true> | ||
| <<<blocks, kBlockSize, 0, stream>>>( | ||
| a.data_ptr<input_t>(), | ||
| b.data_ptr<input_t>(), | ||
| output.data_ptr<output_t>(), | ||
| m, | ||
| n, | ||
| k); | ||
| } else { | ||
| det_gemm_kernel<input_t, output_t, true, false, false> | ||
| <<<blocks, kBlockSize, 0, stream>>>( | ||
| a.data_ptr<input_t>(), | ||
| b.data_ptr<input_t>(), | ||
| output.data_ptr<output_t>(), | ||
| m, | ||
| n, | ||
| k); | ||
| } | ||
| } else if (transpose_b) { | ||
| det_gemm_kernel<input_t, output_t, false, true, false> | ||
| <<<blocks, kBlockSize, 0, stream>>>( | ||
| a.data_ptr<input_t>(), | ||
| b.data_ptr<input_t>(), | ||
| output.data_ptr<output_t>(), | ||
| m, | ||
| n, | ||
| k); | ||
| } else { | ||
| det_gemm_kernel<input_t, output_t, false, false, false> | ||
| <<<blocks, kBlockSize, 0, stream>>>( | ||
| a.data_ptr<input_t>(), | ||
| b.data_ptr<input_t>(), | ||
| output.data_ptr<output_t>(), | ||
| m, | ||
| n, | ||
| k); | ||
| } | ||
| } | ||
|
|
||
| void check_inputs(torch::Tensor a, torch::Tensor b) { | ||
| TORCH_CHECK( | ||
| a.device().type() == c10::kPrivateUse1 && | ||
| b.device().type() == c10::kPrivateUse1, | ||
| "det_gemm requires MUSA tensors"); | ||
| TORCH_CHECK(a.device() == b.device(), "det_gemm tensors must share a device"); | ||
| TORCH_CHECK(a.dim() == 2 && b.dim() == 2, "det_gemm expects 2-D tensors"); | ||
| TORCH_CHECK(a.scalar_type() == b.scalar_type(), "det_gemm dtypes must match"); | ||
| TORCH_CHECK( | ||
| a.scalar_type() == at::ScalarType::BFloat16, | ||
| "MUSA det_gemm currently supports bfloat16 inputs"); | ||
| } | ||
|
|
||
| torch::Tensor dispatch( | ||
| torch::Tensor a, | ||
| torch::Tensor b, | ||
| int m, | ||
| int n, | ||
| int k, | ||
| bool transpose_a, | ||
| bool transpose_b, | ||
| bool transpose_output, | ||
| bool output_fp32) { | ||
| const c10::DeviceGuard device_guard(a.device()); | ||
| a = a.contiguous(); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add a device guard for a.device() at the start of dispatch(). Otherwise, inputs on musa:1 with musa:0 current will launch the kernel on the wrong device/stream. Please cover this with a two-device regression test.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you!Fixed. Added c10::DeviceGuard(a.device()) at the start of dispatch() and added a two-device regression test covering inputs on musa:1 while the current device is musa:0. Local tests pass. |
||
| b = b.contiguous(); | ||
| auto options = output_fp32 ? a.options().dtype(torch::kFloat) : a.options(); | ||
| auto output = torch::empty( | ||
| transpose_output ? std::vector<int64_t>{n, m} | ||
| : std::vector<int64_t>{m, n}, | ||
| options); | ||
| if (m == 0 || n == 0) { | ||
| return output; | ||
| } | ||
| if (output_fp32) { | ||
| launch<c10::BFloat16, float>( | ||
| a, b, output, m, n, k, transpose_a, transpose_b, transpose_output); | ||
| } else { | ||
| launch<c10::BFloat16, c10::BFloat16>( | ||
| a, b, output, m, n, k, transpose_a, transpose_b, transpose_output); | ||
| } | ||
| C10_MUSA_KERNEL_LAUNCH_CHECK(); | ||
| return output; | ||
| } | ||
|
|
||
| } // namespace | ||
|
|
||
| torch::Tensor det_gemm_fwd(torch::Tensor a, torch::Tensor b) { | ||
| check_inputs(a, b); | ||
| TORCH_CHECK(b.size(0) == a.size(1), "det_gemm_fwd: K mismatch"); | ||
| return dispatch(a, b, a.size(0), b.size(1), a.size(1), false, false, false, false); | ||
| } | ||
|
|
||
| torch::Tensor det_gemm_fwd_fp32(torch::Tensor a, torch::Tensor b) { | ||
| check_inputs(a, b); | ||
| TORCH_CHECK(b.size(0) == a.size(1), "det_gemm_fwd_fp32: K mismatch"); | ||
| return dispatch(a, b, a.size(0), b.size(1), a.size(1), false, false, false, true); | ||
| } | ||
|
|
||
| torch::Tensor det_gemm_fwd_rhs_transposed(torch::Tensor a, torch::Tensor bt) { | ||
| check_inputs(a, bt); | ||
| TORCH_CHECK(bt.size(1) == a.size(1), "det_gemm_fwd_rhs_transposed: K mismatch"); | ||
| return dispatch(a, bt, a.size(0), bt.size(0), a.size(1), false, true, false, false); | ||
| } | ||
|
|
||
| torch::Tensor det_gemm_da(torch::Tensor dc, torch::Tensor b) { | ||
| check_inputs(dc, b); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
ast-grep outline rl_engine/kernels/ops/musa/matmul/det_gemm.py --match '_MusaDetGemmFunction' --view expanded
rg -n -C 8 'class _MusaDetGemmFunction|def backward|grad_output|det_gemm_da|det_gemm_db' rl_engine/kernels/ops/musa/matmul/det_gemm.pyRepository: RL-Align/RL-Kernel Length of output: 2680 Convert the FP32 gradient before the native backward calls. When 🤖 Prompt for AI Agents |
||
| TORCH_CHECK(b.size(1) == dc.size(1), "det_gemm_da: N mismatch"); | ||
| return dispatch(dc, b, dc.size(0), b.size(0), dc.size(1), false, true, false, false); | ||
| } | ||
|
|
||
| torch::Tensor det_gemm_db(torch::Tensor a, torch::Tensor dc) { | ||
| check_inputs(a, dc); | ||
| TORCH_CHECK(a.size(0) == dc.size(0), "det_gemm_db: M mismatch"); | ||
| return dispatch(a, dc, a.size(1), dc.size(1), a.size(0), true, false, false, false); | ||
| } | ||
|
|
||
| torch::Tensor det_gemm_db_transposed(torch::Tensor a, torch::Tensor dc) { | ||
| check_inputs(a, dc); | ||
| TORCH_CHECK(a.size(0) == dc.size(0), "det_gemm_db_transposed: M mismatch"); | ||
| return dispatch(a, dc, a.size(1), dc.size(1), a.size(0), true, false, true, false); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| // Copyright (c) 2026 RL-Kernel Contributors | ||
|
|
||
| #include <torch/extension.h> | ||
|
|
||
| torch::Tensor det_gemm_fwd(torch::Tensor a, torch::Tensor b); | ||
| torch::Tensor det_gemm_fwd_fp32(torch::Tensor a, torch::Tensor b); | ||
| torch::Tensor det_gemm_fwd_rhs_transposed(torch::Tensor a, torch::Tensor bt); | ||
| torch::Tensor det_gemm_da(torch::Tensor dc, torch::Tensor b); | ||
| torch::Tensor det_gemm_db(torch::Tensor a, torch::Tensor dc); | ||
| torch::Tensor det_gemm_db_transposed(torch::Tensor a, torch::Tensor dc); | ||
|
|
||
| PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) { | ||
| m.def("det_gemm_fwd", &det_gemm_fwd); | ||
| m.def("det_gemm_fwd_fp32", &det_gemm_fwd_fp32); | ||
| m.def("det_gemm_fwd_rhs_transposed", &det_gemm_fwd_rhs_transposed); | ||
| m.def("det_gemm_da", &det_gemm_da); | ||
| m.def("det_gemm_db", &det_gemm_db); | ||
| m.def("det_gemm_db_transposed", &det_gemm_db_transposed); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| # SPDX-License-Identifier: Apache-2.0 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| from .det_gemm import MusaDetGemmOp | ||
|
|
||
| __all__ = ["MusaDetGemmOp"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # Copyright (c) 2026 RL-Kernel Contributors | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import torch | ||
|
|
||
| from rl_engine.kernels.ops.base import _C, _EXT_AVAILABLE | ||
|
|
||
|
|
||
| class _MusaDetGemmFunction(torch.autograd.Function): | ||
| @staticmethod | ||
| def forward(ctx, a: torch.Tensor, b: torch.Tensor, output_fp32: bool): | ||
| ctx.save_for_backward(a, b) | ||
| ctx.output_fp32 = bool(output_fp32) | ||
| if ctx.output_fp32: | ||
| return _C.det_gemm_fwd_fp32(a, b) | ||
| return _C.det_gemm_fwd(a, b) | ||
|
|
||
| @staticmethod | ||
| def backward(ctx, grad_output: torch.Tensor): | ||
| a, b = ctx.saved_tensors | ||
| grad_a = _C.det_gemm_da(grad_output, b) if ctx.needs_input_grad[0] else None | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. forward_fp32() produces an FP32 gradient, but det_gemm_da/db only accept BF16. So backward will fail here. |
||
| grad_b = _C.det_gemm_db(a, grad_output) if ctx.needs_input_grad[1] else None | ||
|
Comment on lines
+23
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
# Run on a host with the repository's configured MUSA runtime after adding
# the FP32-output backward regression test.
pytest -q tests/test_musa_det_gemm.py -k forward_fp32_backwardRepository: RL-Align/RL-Kernel Length of output: 246 🤖 get_repo_knowledge executed:
Length of output: 44958 🏁 Script executed: #!/bin/bash
set -eu
file="rl_engine/kernels/ops/musa/matmul/det_gemm.py"
printf '%s\n' "== target file =="
cat -n "$file"
printf '%s\n' "== related definitions and tests =="
rg -n -C 4 "det_gemm_(fwd|da|db)|forward_fp32|DetGemm|det_gemm" rl_engine tests 2>/dev/null | head -n 300Repository: RL-Align/RL-Kernel Length of output: 27538 🏁 Script executed: #!/bin/bash
set -eu
file="rl_engine/kernels/ops/musa/matmul/det_gemm.py"
cat -n "$file"
rg -n -C 4 "det_gemm_(fwd|da|db)|forward_fp32|DetGemm|det_gemm" rl_engine tests 2>/dev/null | head -n 300Repository: RL-Align/RL-Kernel Length of output: 27717 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '== target =='
sed -n '1,180p' rl_engine/kernels/ops/musa/matmul/det_gemm.py
printf '%s\n' '== symbols =='
rg -n -C 5 'det_gemm_(fwd|da|db)|forward_fp32|det_gemm' . --glob '!*.lock' | head -n 400Repository: RL-Align/RL-Kernel Length of output: 33806 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '== MUSA GEMM implementation =='
sed -n '1,180p' csrc/musa/det_gemm.mu
printf '%s\n' '== MUSA tests =='
find . -maxdepth 4 -type f \( -iname '*musa*' -o -iname '*det_gemm*' \) -print
rg -n -C 5 'MusaDetGemmOp|forward_fp32|det_gemm_fwd_fp32|det_gemm_da|det_gemm_db' tests rl_engine/tests csrc/musa rl_engine/_C.pyi 2>/dev/null | head -n 300Repository: RL-Align/RL-Kernel Length of output: 25361 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '== CUDA det_gemm backward policy =='
sed -n '1,180p' rl_engine/kernels/ops/cuda/matmul/det_gemm.py
printf '%s\n' '== MUSA det_gemm tests =='
sed -n '1,260p' tests/test_musa_det_gemm.pyRepository: RL-Align/RL-Kernel Length of output: 9278 Cast 🤖 Prompt for AI Agents |
||
| return grad_a, grad_b, None | ||
|
|
||
|
|
||
| class _MusaDetLinearFunction(torch.autograd.Function): | ||
| @staticmethod | ||
| def forward(ctx, a: torch.Tensor, weight: torch.Tensor): | ||
| ctx.save_for_backward(a, weight) | ||
| return _C.det_gemm_fwd_rhs_transposed(a, weight) | ||
|
|
||
| @staticmethod | ||
| def backward(ctx, grad_output: torch.Tensor): | ||
| a, weight = ctx.saved_tensors | ||
| grad_a = _C.det_gemm_fwd(grad_output, weight) if ctx.needs_input_grad[0] else None | ||
| grad_weight = _C.det_gemm_db_transposed(a, grad_output) if ctx.needs_input_grad[1] else None | ||
| return grad_a, grad_weight | ||
|
|
||
|
|
||
| class MusaDetGemmOp: | ||
| """Deterministic fixed-order GEMM for MUSA BF16 tensors.""" | ||
|
|
||
| def __init__(self) -> None: | ||
| if not _EXT_AVAILABLE or _C is None: | ||
| raise RuntimeError("MUSA det_gemm requires the compiled extension") | ||
| required = ( | ||
| "det_gemm_fwd", | ||
| "det_gemm_fwd_fp32", | ||
| "det_gemm_fwd_rhs_transposed", | ||
| "det_gemm_da", | ||
| "det_gemm_db", | ||
| "det_gemm_db_transposed", | ||
| ) | ||
| missing = [name for name in required if not hasattr(_C, name)] | ||
| if missing: | ||
| raise RuntimeError(f"MUSA det_gemm extension is missing: {', '.join(missing)}") | ||
|
|
||
| @staticmethod | ||
| def _check(a: torch.Tensor, b: torch.Tensor) -> None: | ||
| if a.device.type != "musa" or b.device.type != "musa": | ||
| raise ValueError("MUSA det_gemm requires MUSA tensors") | ||
| if a.dtype != torch.bfloat16 or b.dtype != torch.bfloat16: | ||
| raise TypeError("MUSA det_gemm currently supports BF16 tensors") | ||
|
|
||
| def __call__(self, a: torch.Tensor, b: torch.Tensor) -> torch.Tensor: | ||
| self._check(a, b) | ||
| if a.ndim != 2 or b.ndim != 2 or a.size(1) != b.size(0): | ||
| raise ValueError("det_gemm expects A[M,K] and B[K,N]") | ||
| return _MusaDetGemmFunction.apply(a.contiguous(), b.contiguous(), False) | ||
|
|
||
| def forward_fp32(self, a: torch.Tensor, b: torch.Tensor) -> torch.Tensor: | ||
| self._check(a, b) | ||
| if a.ndim != 2 or b.ndim != 2 or a.size(1) != b.size(0): | ||
| raise ValueError("det_gemm expects A[M,K] and B[K,N]") | ||
| return _MusaDetGemmFunction.apply(a.contiguous(), b.contiguous(), True) | ||
|
|
||
| forward_accum_fp32 = forward_fp32 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. forward_accum_fp32 is an alias for a BF16-only method, but callers can pass FP32 intermediates to it. That would fail on MUSA. |
||
|
|
||
| def linear(self, a: torch.Tensor, weight: torch.Tensor) -> torch.Tensor: | ||
| self._check(a, weight) | ||
| if a.ndim != 2 or weight.ndim != 2 or a.size(1) != weight.size(1): | ||
| raise ValueError("linear expects A[M,K] and weight[N,K]") | ||
| return _MusaDetLinearFunction.apply(a.contiguous(), weight.contiguous()) | ||
|
|
||
|
|
||
| def deterministic_gemm(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor: | ||
| return MusaDetGemmOp()(a, b) | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
Use size-safe launch and kernel indexing.
For example, a valid BF16 output with
m=65536andn=32768requires 4 GiB, butm * noverflowsinthere. The same product controls the kernel bounds. The launch can fail or leave the output unwritten. Use 64-bit dimensions and indices throughout the launch and kernel, or reject shapes above a checked limit before allocating the output.🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@maxiaosong1124, agreed. Casting the result ofm * nis too late. The operands must be 64-bit before multiplication. Input offsets such asrow * kneed the same protection.int64_tdimensions and indices throughoutcsrc/musa/det_gemm.mu.You are interacting with an AI system.