Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 additions & 0 deletions cpp/tensorrt_llm/kernels/minimaxM3Fp8IndexerKernel.cu
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*
* Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#include "minimaxM3Fp8IndexerKernel.h"

#include "tensorrt_llm/common/cudaUtils.h"
#include "tensorrt_llm/common/mathUtils.h"
#include "tensorrt_llm/common/reduceKernelUtils.cuh"

#include <cuda_bf16.h>
#include <cuda_fp8.h>
#include <cuda_runtime.h>

#include <cstdint>

TRTLLM_NAMESPACE_BEGIN

namespace kernels
{

namespace
{

constexpr int kHeadDim = 128;
constexpr int kRotaryDim = 64;
constexpr int kElemsPerThread = kHeadDim / 32;

// Match the established vLLM contract exactly: the normalized/RoPE result is
// first materialized as BF16 and then cast, without an external FP8 scale.
__device__ __forceinline__ __nv_fp8_e4m3 bf16RoundedToFp8(float value)
{
return __nv_fp8_e4m3(__bfloat162float(__float2bfloat16_rn(value)));
}

__global__ void minimaxM3Fp8IndexerQKNormRopeKernel(__nv_bfloat16 const* qk, __nv_fp8_e4m3* q_out,
__nv_fp8_e4m3* k_cache, int const* out_cache_loc, int64_t page_stride, int64_t token_stride, int page_size,
int num_tokens, int num_heads_q, float eps, __nv_bfloat16 const* q_weight, __nv_bfloat16 const* k_weight,
float base, int const* position_ids)
{
int const warps_per_block = blockDim.x / 32;
int const warp_id = threadIdx.x / 32;
int const lane_id = threadIdx.x % 32;
int const global_warp = blockIdx.x * warps_per_block + warp_id;
int const total_heads = num_heads_q + 1;
int const token_idx = global_warp / total_heads;
int const local_head = global_warp % total_heads;
if (token_idx >= num_tokens)
{
return;
}

bool const is_q = local_head < num_heads_q;
int64_t const input_offset
= (static_cast<int64_t>(token_idx) * total_heads + local_head) * kHeadDim + lane_id * kElemsPerThread;

uint2 const packed_input = *reinterpret_cast<uint2 const*>(qk + input_offset);
float elements[kElemsPerThread];
float sum_squares = 0.0F;
#pragma unroll
for (int pair = 0; pair < 2; ++pair)
{
auto const values = __bfloat1622float2(reinterpret_cast<__nv_bfloat162 const*>(&packed_input)[pair]);
elements[pair * 2] = values.x;
elements[pair * 2 + 1] = values.y;
sum_squares += values.x * values.x + values.y * values.y;
}

sum_squares = tensorrt_llm::common::warpReduceSum(sum_squares);
float const rms_rcp = rsqrtf(sum_squares / static_cast<float>(kHeadDim) + eps);
auto const* weight = is_q ? q_weight : k_weight;
#pragma unroll
for (int i = 0; i < kElemsPerThread; ++i)
{
int const dim = lane_id * kElemsPerThread + i;
elements[i] *= rms_rcp * (1.0F + __bfloat162float(weight[dim]));
}

// MiniMax-M3 uses NeoX partial RoPE: rotate the first 64 of 128 channels.
// Four elements per lane means the matching half is eight lanes away.
__syncwarp();
constexpr int kPairOffset = (kRotaryDim / 2) / kElemsPerThread;
#pragma unroll
for (int i = 0; i < kElemsPerThread; ++i)
{
int const dim = lane_id * kElemsPerThread + i;
float paired = __shfl_xor_sync(0xffffffff, elements[i], kPairOffset);
if (dim < kRotaryDim)
{
if (lane_id < kPairOffset)
{
paired = -paired;
}
int const dim_idx = (dim * 2) % kRotaryDim;
int const half_dim = dim_idx / 2;
float const frequency = powf(base, -2.0F * half_dim / static_cast<float>(kRotaryDim));
float sine;
float cosine;
__sincosf(static_cast<float>(position_ids[token_idx]) * frequency, &sine, &cosine);
elements[i] = elements[i] * cosine + paired * sine;
}
}
__syncwarp();

uint32_t packed_output = 0;
auto* fp8_values = reinterpret_cast<__nv_fp8_e4m3*>(&packed_output);
#pragma unroll
for (int i = 0; i < kElemsPerThread; ++i)
{
fp8_values[i] = bf16RoundedToFp8(elements[i]);
}

__nv_fp8_e4m3* output;
if (is_q)
{
int64_t const output_offset
= (static_cast<int64_t>(token_idx) * num_heads_q + local_head) * kHeadDim + lane_id * kElemsPerThread;
output = q_out + output_offset;
}
else
{
int const slot = out_cache_loc[token_idx];
int const page = slot / page_size;
int const within_page = slot % page_size;
output = k_cache + static_cast<int64_t>(page) * page_stride + static_cast<int64_t>(within_page) * token_stride
+ lane_id * kElemsPerThread;
}
*reinterpret_cast<uint32_t*>(output) = packed_output;
}

} // namespace

void launchMinimaxM3Fp8IndexerQKNormRope(void const* qk, void* q_out, void* k_cache, int const* out_cache_loc,
int64_t page_stride, int64_t token_stride, int page_size, int num_tokens, int num_heads_q, int head_dim,
int rotary_dim, float eps, void const* q_weight, void const* k_weight, float base, int const* position_ids,
cudaStream_t stream)
{
TLLM_CHECK_WITH_INFO(head_dim == kHeadDim, "MiniMax-M3 FP8 indexer requires head_dim=128");
TLLM_CHECK_WITH_INFO(rotary_dim == kRotaryDim, "MiniMax-M3 FP8 indexer requires rotary_dim=64");
TLLM_CHECK_WITH_INFO(num_heads_q > 0, "MiniMax-M3 FP8 indexer requires at least one query head");

constexpr int kBlockSize = 256;
constexpr int kWarpsPerBlock = kBlockSize / 32;
int const total_warps = num_tokens * (num_heads_q + 1);
int const grid_size = common::divUp(total_warps, kWarpsPerBlock);
minimaxM3Fp8IndexerQKNormRopeKernel<<<grid_size, kBlockSize, 0, stream>>>(static_cast<__nv_bfloat16 const*>(qk),
static_cast<__nv_fp8_e4m3*>(q_out), static_cast<__nv_fp8_e4m3*>(k_cache), out_cache_loc, page_stride,
token_stride, page_size, num_tokens, num_heads_q, eps, static_cast<__nv_bfloat16 const*>(q_weight),
static_cast<__nv_bfloat16 const*>(k_weight), base, position_ids);
}

} // namespace kernels

TRTLLM_NAMESPACE_END
41 changes: 41 additions & 0 deletions cpp/tensorrt_llm/kernels/minimaxM3Fp8IndexerKernel.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#pragma once

#include "tensorrt_llm/common/config.h"

#include <cstdint>
#include <cuda_runtime.h>

TRTLLM_NAMESPACE_BEGIN

namespace kernels
{

// MiniMax-M3-specific index-branch producer. It applies Gemma RMSNorm and
// NeoX partial RoPE to a packed BF16 [index-Q | index-K] projection, writes
// index-Q as unscaled E4M3, and inserts index-K directly into the paged E4M3
// HND cache. The direct cache store removes the standalone cast/scatter launch
// from the decode graph.
void launchMinimaxM3Fp8IndexerQKNormRope(void const* qk, void* q_out, void* k_cache, int const* out_cache_loc,
int64_t page_stride, int64_t token_stride, int page_size, int num_tokens, int num_heads_q, int head_dim,
int rotary_dim, float eps, void const* q_weight, void const* k_weight, float base, int const* position_ids,
cudaStream_t stream);

} // namespace kernels

TRTLLM_NAMESPACE_END
1 change: 1 addition & 0 deletions cpp/tensorrt_llm/thop/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ add_library(
IndexerKCacheScatterOp.cpp
IndexerTopKOp.cpp
minimaxM3SelectBlocksOp.cpp
minimaxM3Fp8IndexerOp.cpp
mlaRopeInplaceOp.cpp
ncclCommunicatorOp.cpp
allocateOutput.cpp
Expand Down
90 changes: 90 additions & 0 deletions cpp/tensorrt_llm/thop/minimaxM3Fp8IndexerOp.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#include "tensorrt_llm/kernels/minimaxM3Fp8IndexerKernel.h"
#include "tensorrt_llm/thop/thUtils.h"

#include <ATen/cuda/CUDAContext.h>
#include <torch/extension.h>

TRTLLM_NAMESPACE_BEGIN

namespace torch_ext
{

torch::Tensor minimax_m3_fp8_indexer_qk_norm_rope(torch::Tensor const& qk, torch::Tensor& index_k_cache,
torch::Tensor const& out_cache_loc, int64_t num_heads_q, int64_t head_dim, int64_t rotary_dim, double eps,
torch::Tensor const& q_weight, torch::Tensor const& k_weight, double base, torch::Tensor const& position_ids)
{
TORCH_CHECK(qk.dim() == 2, "Index QK must be [num_tokens, (num_heads_q + 1) * head_dim]");
TORCH_CHECK(index_k_cache.dim() == 4 && index_k_cache.size(1) == 1,
"Index-K cache must be HND [num_pages, 1, page_size, head_dim]");
TORCH_CHECK(out_cache_loc.dim() == 1, "out_cache_loc must be one-dimensional");
TORCH_CHECK(position_ids.dim() == 1, "position_ids must be one-dimensional");
TORCH_CHECK(q_weight.dim() == 1 && k_weight.dim() == 1, "Q/K norm weights must be one-dimensional");

CHECK_INPUT(qk, torch::kBFloat16);
CHECK_INPUT(out_cache_loc, torch::kInt32);
CHECK_INPUT(position_ids, torch::kInt32);
CHECK_INPUT(q_weight, torch::kBFloat16);
CHECK_INPUT(k_weight, torch::kBFloat16);
TORCH_CHECK(index_k_cache.is_cuda(), "Index-K cache must be on CUDA");
TORCH_CHECK(
index_k_cache.scalar_type() == at::ScalarType::Float8_e4m3fn, "Index-K cache must use torch.float8_e4m3fn");

int64_t const num_tokens = qk.size(0);
TORCH_CHECK(qk.size(1) == (num_heads_q + 1) * head_dim, "Index QK width must equal (num_heads_q + 1) * head_dim");
TORCH_CHECK(index_k_cache.size(3) == head_dim, "Index-K cache head dimension mismatch");
TORCH_CHECK(index_k_cache.stride(3) == 1 && index_k_cache.stride(2) == head_dim,
"Index-K cache must have contiguous token rows in HND layout");
TORCH_CHECK(out_cache_loc.numel() >= num_tokens, "out_cache_loc is shorter than num_tokens");
TORCH_CHECK(position_ids.numel() == num_tokens, "position_ids length must equal num_tokens");
TORCH_CHECK(
q_weight.numel() == head_dim && k_weight.numel() == head_dim, "Q/K norm weight width must equal head_dim");
TORCH_CHECK(qk.get_device() == index_k_cache.get_device() && qk.get_device() == out_cache_loc.get_device()
&& qk.get_device() == position_ids.get_device() && qk.get_device() == q_weight.get_device()
&& qk.get_device() == k_weight.get_device(),
"All MiniMax-M3 FP8 indexer tensors must be on the same CUDA device");

auto q_out = torch::empty({num_tokens, num_heads_q, head_dim}, qk.options().dtype(at::ScalarType::Float8_e4m3fn));
if (num_tokens == 0)
{
return q_out;
}
auto stream = at::cuda::getCurrentCUDAStream(qk.get_device());
tensorrt_llm::kernels::launchMinimaxM3Fp8IndexerQKNormRope(qk.data_ptr(), q_out.data_ptr(),
index_k_cache.data_ptr(), out_cache_loc.data_ptr<int>(), index_k_cache.stride(0), index_k_cache.stride(2),
index_k_cache.size(2), num_tokens, num_heads_q, head_dim, rotary_dim, static_cast<float>(eps),
q_weight.data_ptr(), k_weight.data_ptr(), static_cast<float>(base), position_ids.data_ptr<int>(), stream);
return q_out;
}

TORCH_LIBRARY_FRAGMENT(trtllm, m)
{
m.def(
"minimax_m3_fp8_indexer_qk_norm_rope(Tensor qk, Tensor(a!) index_k_cache, Tensor out_cache_loc, int "
"num_heads_q, int head_dim, int rotary_dim, float eps, Tensor q_weight, Tensor k_weight, float base, Tensor "
"position_ids) -> Tensor");
}

TORCH_LIBRARY_IMPL(trtllm, CUDA, m)
{
m.impl("minimax_m3_fp8_indexer_qk_norm_rope", &minimax_m3_fp8_indexer_qk_norm_rope);
}

} // namespace torch_ext

TRTLLM_NAMESPACE_END
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,9 @@ def __init__(
# then from ``sparse_attn_config``, then from the M3 checkpoint
# convention (layers 0..2 dense, 3..N-1 sparse,
# disable_index_value=True, sparse_index_dim=128).
sparse_attn_config = kwargs.get("sparse_attn_config")
sparse_attn_config = kwargs.get("sparse_attn_config") or kwargs.get(
"sparse_attention_config"
)
num_layers = kwargs.get("num_layers")

if sparse_index_dim is None:
Expand All @@ -195,6 +197,19 @@ def __init__(
self.sparse_layer_ids = sorted(int(i) for i in sparse_layer_ids)
self.disable_index_value_layer_ids = set(int(i) for i in disable_index_value_layer_ids)
self.sparse_index_dim = int(sparse_index_dim)
self.indexer_kv_dtype = str(getattr(sparse_attn_config, "indexer_kv_dtype", "bf16"))
if self.indexer_kv_dtype not in ("bf16", "fp8"):
raise ValueError(
"MiniMax M3 indexer_kv_dtype must be 'bf16' or 'fp8', got "
f"{self.indexer_kv_dtype!r}."
)
if self.indexer_kv_dtype == "fp8" and (
set(self.sparse_layer_ids) - self.disable_index_value_layer_ids
):
raise ValueError(
"MiniMax M3 FP8 index cache requires disable_index_value=True "
"for every sparse layer."
)

super().__init__(*args, **kwargs)

Expand Down Expand Up @@ -265,7 +280,9 @@ def _compute_num_total_slots(self) -> int:
return int((page_upper // kv_factor) * self.tokens_per_block)

def _torch_dtype_for_index_cache(self) -> torch.dtype:
"""Match the main cache dtype where possible, fall back to bf16."""
"""Return the independently configured index-cache storage dtype."""
if self.indexer_kv_dtype == "fp8":
return torch.float8_e4m3fn
if self.dtype == DataType.HALF:
return torch.float16
if self.dtype == DataType.FLOAT:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class MiniMaxM3SparseParams(SparseParams):
score_type: str = "max"
disable_index_value: bool = True
implementation: Literal["triton", "msa"] = "triton"
indexer_kv_dtype: Literal["bf16", "fp8"] = "bf16"

@property
def indices_block_size(self) -> int:
Expand Down
Loading
Loading