Skip to content

Commit 45a2bc2

Browse files
committed
[None][perf] DSV4 o_proj: fuse fp8/UE8M0 quantize into o_a CuTe-DSL epilogue (gated)
Fold the o_lora bf16->fp8 + 1x128 packed-UE8M0 quantization into the o_a blockwise GEMM epilogue (single-pass, tree-reduce amax), so o_a emits fp8+sf directly and o_b (DeepGEMM 1d1d) consumes it without the separate fp8_quantize_1x128_packed kernel and the bf16 o_lora HBM round-trip. - blockwise_gemm.py: optional fp8+SF epilogue path, gated by sf_out tensor; the bf16 path is byte-identical when off. Single-pass register cache, tree-reduce amax (byte-exact), packed-UE8M0 MN-major sf store, predicated for tail-M, config-guarded to (128,128)/1-CTA. - cute_dsl_custom_ops.py: new trtllm::cute_dsl_fp8_bmm_blackwell_fp8out op. - attention.py: gated DSV4_FUSE_OPROJ branch in _deepseek_v4_o_proj (tp_size==1), falls back to the existing path when the flag is off. Read has_fp8_block_scales as a property (it is @Property), not as a call. - test_deepseek_v4_o_proj.py: add test_deepseek_v4_o_proj_fused_fp8_equivalence (fused vs unfused, bit-exact across M=1/16/128/256); share setup via a helper. Validated bit-exact fused-vs-unfused (diff=0 across M) and within the fp8 reference bar (B200), net-positive (+1.6us/o_a in decode, nsys hardware-timestamp measured). Default OFF -> zero functional change unless DSV4_FUSE_OPROJ=1. Signed-off-by: Mingyang Hao <mingyangh@nvidia.com>
1 parent ec3da87 commit 45a2bc2

4 files changed

Lines changed: 780 additions & 59 deletions

File tree

tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3849,6 +3849,111 @@ def _(
38493849
assert output.shape == (batch_size, m,
38503850
n), "CuTe DSL fp8 bmm output shape is incorrect"
38513851

3852+
# a/b: fp8, scale: fp32; output: fp8 e4m3 + packed-UE8M0 1x128 scale factors.
3853+
# Fused-o_proj producer: emits the FLATTENED [M, L*N] o_lora as fp8+sf directly,
3854+
# ready for deep_gemm.fp8_gemm_nt with no separate quantize kernel + bf16
3855+
# round-trip. The (fp8, sf) bytes are bit-identical to
3856+
# per_token_cast_to_fp8(use_ue8m0=True) + get_mn_major_tma_aligned_packed_ue8m0.
3857+
# Fixed (128,128)/1-CTA tactic, matching o_a's locked config.
3858+
@torch.library.custom_op("trtllm::cute_dsl_fp8_bmm_blackwell_fp8out",
3859+
mutates_args=("output_fp8", "sf_out"),
3860+
device_types="cuda")
3861+
def cute_dsl_fp8_bmm_blackwell_fp8out(
3862+
input: torch.Tensor, # [L, M, K] e4m3
3863+
weight: torch.Tensor, # [L, N, K] e4m3 (per-group N = o_lora_rank)
3864+
input_scale: torch.
3865+
Tensor, # fp32, same layout as cute_dsl_fp8_bmm_blackwell
3866+
weight_scale: torch.Tensor, # fp32
3867+
output_fp8: torch.Tensor, # [L, M, N] e4m3 (mutated)
3868+
sf_out: torch.
3869+
Tensor, # [M, NKB4] int32, MN-major stride (1, aligned_mn) (mutated)
3870+
) -> None:
3871+
if not is_sm_100f():
3872+
raise ValueError(
3873+
f"cute_dsl_fp8_bmm_blackwell_fp8out requires SM100 family, got {get_sm_version()}"
3874+
)
3875+
from ..cute_dsl_kernels.blackwell.blockwise_gemm.blockwise_gemm import \
3876+
Sm100BlockwiseGemmKernel
3877+
from ..cute_dsl_kernels.blackwell.utils import AddressSpace, make_ptr
3878+
3879+
L, M, K = input.shape[0], input.shape[1], input.shape[2]
3880+
N = weight.shape[1]
3881+
sf_m = pad_up(M, 4)
3882+
sf_k = ceil_div(K, 128)
3883+
sf_n = ceil_div(N, 128)
3884+
aligned_mn = pad_up(M, 4) # == get_tma_aligned_size(M, 4) for int32
3885+
n_tiles_per_group = ceil_div(
3886+
N, 128) # per-group N-tiles; global kblock = group*this + local
3887+
3888+
gemm = Sm100BlockwiseGemmKernel(
3889+
cutlass.Float32,
3890+
use_2cta_instrs=False,
3891+
mma_tiler_mn=(128, 128),
3892+
cluster_shape_mn=(1, 1),
3893+
)
3894+
max_active_clusters = cutlass.utils.HardwareInfo(
3895+
).get_max_active_clusters(1)
3896+
3897+
a_ptr = make_ptr(cutlass.Float8E4M3FN,
3898+
input.data_ptr(),
3899+
AddressSpace.gmem,
3900+
assumed_align=16)
3901+
b_ptr = make_ptr(cutlass.Float8E4M3FN,
3902+
weight.data_ptr(),
3903+
AddressSpace.gmem,
3904+
assumed_align=16)
3905+
a_sf_ptr = make_ptr(cutlass.Float32,
3906+
input_scale.data_ptr(),
3907+
AddressSpace.gmem,
3908+
assumed_align=16)
3909+
b_sf_ptr = make_ptr(cutlass.Float32,
3910+
weight_scale.data_ptr(),
3911+
AddressSpace.gmem,
3912+
assumed_align=16)
3913+
c_tmp = output_fp8.permute(1, 2, 0)
3914+
c_cute = cute.runtime.from_dlpack(c_tmp).mark_layout_dynamic(
3915+
leading_dim=1)
3916+
sf_out_ptr = make_ptr(cutlass.Int32,
3917+
sf_out.data_ptr(),
3918+
AddressSpace.gmem,
3919+
assumed_align=4)
3920+
stream = cuda.CUstream(torch.cuda.current_stream().cuda_stream)
3921+
3922+
gemm.wrapper_fp8sf(
3923+
M,
3924+
N,
3925+
K,
3926+
sf_m,
3927+
sf_n,
3928+
sf_k,
3929+
L,
3930+
a_ptr,
3931+
b_ptr,
3932+
a_sf_ptr,
3933+
b_sf_ptr,
3934+
c_cute,
3935+
max_active_clusters,
3936+
stream,
3937+
sf_out_ptr,
3938+
aligned_mn,
3939+
n_tiles_per_group,
3940+
)
3941+
3942+
@torch.library.register_fake("trtllm::cute_dsl_fp8_bmm_blackwell_fp8out")
3943+
def _(
3944+
input: torch.Tensor,
3945+
weight: torch.Tensor,
3946+
input_scale: torch.Tensor,
3947+
weight_scale: torch.Tensor,
3948+
output_fp8: torch.Tensor,
3949+
sf_out: torch.Tensor,
3950+
) -> None:
3951+
L, M, K = input.shape
3952+
N = weight.shape[1]
3953+
assert output_fp8.dtype == torch.float8_e4m3fn and output_fp8.shape == (
3954+
L, M, N)
3955+
assert sf_out.dtype == torch.int32
3956+
38523957
# =============================================================================
38533958
# Dense GEMM with SwiGLU Fusion (FC1 Kernel for MoE as Dense GEMM)
38543959
# =============================================================================

0 commit comments

Comments
 (0)