@@ -93,6 +93,62 @@ def _coop_tile_for(M: int, N: int, K: int) -> tuple[int, int, int, int] | None:
9393 return None
9494
9595
96+ def _native_fp8_tile_for (M : int , N : int , K : int ) -> tuple [int , int , int , int ] | None :
97+ """Pick a tile (BM, BN, BK, threads) for the NATIVE fp8-input e4m3 MMA path.
98+
99+ Unlike the Metal cooperative ``matmul2d`` selector (:func:`_coop_tile_for`),
100+ this targets the gb10/sm_121 CUDA ``T.gemm`` SM120 fp8 MMA dispatch
101+ (``SM120_16x8x32_TN`` -> ``mma.sync.aligned.kind::f8f6f4.m16n8k32...
102+ .e4m3.e4m3.f32``). The hardware atom is m16n8k32, so the gating constraints
103+ are (proven on hardware, res_r2mma.feasibility):
104+
105+ * TN layout (transpose_B=True) -- handled by the kernel, not the tile.
106+ * K % 32 == 0 (the fp8 MMA is K=32-deep). HARD FLOOR; BK must be a
107+ multiple of 32 and K a multiple of BK.
108+ * BM % 16 == 0 and BN % 8 == 0 (the m16n8 output fragment shape); we use
109+ the larger, warp-tile-friendly BM % 16, BN % 16 to map cleanly onto
110+ 128/256-thread warp grids.
111+
112+ The candidate tiles are ordered best-first per the research recommendation:
113+ start from the proven tilelang fp8 example geometry (128x128x64, 128 threads)
114+ which divides every prod GEMM (M=16384; N in {3584,10752,37888}; K in
115+ {3584,18944}); fall back to smaller tiles for the narrow SSD F2 shape and any
116+ residual sub-128 dim. Threadgroup shared memory is the e4m3 operands (1 byte)
117+ plus the fp32 C staging -- far under budget, so the dominant constraint is
118+ divisibility, not shmem.
119+
120+ Returns ``None`` when no legal native-fp8 tile divides ``M/N/K`` (e.g. a shape
121+ whose K is not a multiple of 32). RULE #1: the caller RAISES on ``None`` (no
122+ silent partial-tile / no fp16-decode fallback) -- a K%32!=0 shape simply
123+ cannot run the native fp8 MMA and must surface that, not silently degrade.
124+ """
125+ if K % 32 != 0 :
126+ # The fp8 m16n8k32 MMA hard-requires K (and thus BK) a multiple of 32.
127+ # Surface the violation to the caller (it RAISES) rather than picking an
128+ # illegal tile that would mis-tile the K reduction (RULE #1).
129+ return None
130+ candidates = (
131+ (128 , 128 , 64 , 128 ), # research-recommended fp8 example geometry
132+ (128 , 128 , 32 , 128 ), # smaller BK (K=32-deep amortized less)
133+ (128 , 64 , 64 , 128 ), # narrower N
134+ (64 , 64 , 64 , 128 ), # SSD F2 tile (64x64x64) and small dims
135+ (64 , 64 , 32 , 128 ),
136+ (32 , 64 , 32 , 128 ), # current untuned R2 tile (kept as a sweep point)
137+ (16 , 32 , 32 , 64 ), # smallest legal native-fp8 tile (m16n8 fragment)
138+ )
139+ for bm , bn , bk , threads in candidates :
140+ if bk % 32 != 0 :
141+ continue
142+ if M % bm or N % bn or K % bk :
143+ continue
144+ if bm % 16 or bn % 8 :
145+ continue
146+ if (M // bm ) <= 0 or (N // bn ) <= 0 :
147+ continue
148+ return bm , bn , bk , threads
149+ return None
150+
151+
96152@dataclass (frozen = True )
97153class FP8MatmulPathCStatus :
98154 available : bool
@@ -297,6 +353,93 @@ def _fp8_scaled_matmul2d_cuda_kernel_template(
297353 T .copy (C_shared , C [by * _FP8_MM_BM , bx * _FP8_MM_BN ], disable_tma = True ) # type: ignore[name-defined] # noqa: F821
298354
299355
356+ def _fp8_scaled_matmul2d_cuda_native_kernel_template (
357+ A_fp8 : T .Tensor ((_FP8_MM_M , _FP8_MM_K ), "float8_e4m3" ), # type: ignore[name-defined] # noqa: F821
358+ A_scale : T .Tensor ((1 ,), "float32" ), # type: ignore[name-defined] # noqa: F821
359+ B_fp8 : T .Tensor ((_FP8_MM_N , _FP8_MM_K ), "float8_e4m3" ), # type: ignore[name-defined] # noqa: F821
360+ B_scale : T .Tensor ((1 ,), "float32" ), # type: ignore[name-defined] # noqa: F821
361+ C : T .Tensor ((_FP8_MM_M , _FP8_MM_N ), _FP8_MM_C_DTYPE ), # type: ignore[name-defined] # noqa: F821
362+ ):
363+ """NATIVE fp8-input e4m3 tensor-core MMA on gb10 / sm_121 (lever r2-native-fp8mma).
364+
365+ This is the lever-1 rewrite of :func:`_fp8_scaled_matmul2d_cuda_kernel_template`.
366+ The slow twin DECODES each e4m3 byte to ``float16`` in register fragments and
367+ runs ``tl.mma_sync<kFloat16,kFloat16,kFloat32,16,8,16>`` -- an fp16-MMA on
368+ decoded values (the 0.18-0.42x memory-lever). THIS template instead keeps the
369+ e4m3 operands as ``float8_e4m3`` all the way into ``T.gemm``, so TileLang's
370+ CUDA GEMM backend dispatches the SM120 fp8 MMA atom
371+ (``src/backend/cuda/op/gemm.cc`` lines 60/74 accept fp8-input GEMM ->
372+ ``gemm_mma.h`` ``#if __CUDA_ARCH_LIST__ >= 1200`` ->
373+ ``TL_DISPATCH_MMA_TEMPLATE(fp8_e4_t, fp8_e4_t, float, SM120_16x8x32_TN)`` ->
374+ CuTe ``SM120_16x8x32_TN`` -> the real Blackwell PTX
375+ ``mma.sync.aligned.kind::f8f6f4.m16n8k32.row.col.f32.e4m3.e4m3.f32``). The
376+ generated CUDA emits ``tl::mma_sync<kFloat8_e4m3,kFloat8_e4m3,kFloat32,16,8,32>``
377+ over ``fp8_e4_t`` register operands -- a REAL fp8-input MMA, fp32 accumulate,
378+ ZERO fp16 decode (PROVEN on the live gb10 sm_121 GPU, res_r2mma.feasibility=GO;
379+ on-hardware compile+run gave maxabs-err 0.0229 vs an fp16 reference = honest
380+ fp8 quantization noise, RULE #1 clean).
381+
382+ Deltas vs the fp16-decode twin (the ONLY changes -- everything else identical):
383+
384+ * ``A_shared``/``B_shared`` are ``float8_e4m3`` (NOT ``float16``); the e4m3
385+ bytes are copied straight from global into the e4m3 shared buffers
386+ (``disable_tma=True``), with NO ``A_fp8_local``/``B_fp8_local`` fragment
387+ stage and NO ``T.cast(e4m3 -> float16)`` Parallel loops.
388+ * ``T.gemm(A_shared_e4m3, B_shared_e4m3, C_frag_fp32, transpose_B=True)``
389+ now dispatches the native fp8 MMA (the fp8+fp8+fp32 branch of gemm.cc)
390+ instead of the fp16 MMA.
391+
392+ Unchanged: the fp32 ``C_frag`` register accumulator (the fp8 MMA accumulates
393+ in fp32 -- do NOT switch to fp16 accumulate; it loses range), the post-K
394+ ``sa*sb`` scale epilogue, the ``C_shared`` copy-out staging, and the
395+ ``disable_tma=True`` on every copy (sm_121 TMA tensormap mis-aligns at these
396+ tile dims -> "Invalid TMA descriptor arguments"; the cp.async path is correct).
397+ The compile site (:func:`_fp8_matmul_tvm_ffi_kernel_for`) keeps the same
398+ ``tl.disable_tma_lower`` / ``tl.disable_warp_specialized`` pass_configs.
399+
400+ HARD constraints (gemm.cc CheckWgmma fp8 branch + the m16n8k32 MMA shape),
401+ enforced by :func:`_native_fp8_tile_for` (tile=None -> caller RAISES, RULE #1):
402+ transpose_B=True (TN) and K % 32 == 0. All prod shapes satisfy them.
403+ """
404+ with T .Kernel ( # type: ignore[name-defined] # noqa: F821
405+ T .ceildiv (_FP8_MM_N , _FP8_MM_BN ), # type: ignore[name-defined] # noqa: F821
406+ T .ceildiv (_FP8_MM_M , _FP8_MM_BM ), # type: ignore[name-defined] # noqa: F821
407+ threads = _FP8_MM_THREADS ,
408+ ) as (bx , by ):
409+ # NATIVE-fp8: the shared operands stay e4m3 (no float16 decode buffers).
410+ A_shared = T .alloc_shared ((_FP8_MM_BM , _FP8_MM_BK ), "float8_e4m3" , scope = "shared" ) # type: ignore[name-defined] # noqa: F821
411+ B_shared = T .alloc_shared ((_FP8_MM_BN , _FP8_MM_BK ), "float8_e4m3" , scope = "shared" ) # type: ignore[name-defined] # noqa: F821
412+ C_frag = T .alloc_fragment ((_FP8_MM_BM , _FP8_MM_BN ), _FP8_MM_C_DTYPE ) # type: ignore[name-defined] # noqa: F821
413+ C_shared = T .alloc_shared ((_FP8_MM_BM , _FP8_MM_BN ), _FP8_MM_C_DTYPE , scope = "shared" ) # type: ignore[name-defined] # noqa: F821
414+ T .clear (C_frag )
415+ for ko in T .serial (T .ceildiv (_FP8_MM_K , _FP8_MM_BK )): # type: ignore[name-defined] # noqa: F821
416+ # Copy the e4m3 bytes global->shared DIRECTLY (disable_tma on sm_121).
417+ # NO fp16 decode: the e4m3 operands feed T.gemm unchanged so the SM120
418+ # fp8 MMA atom fires. This is the whole point of the lever (RULE #1: a
419+ # real fp8-input MMA, verified by the kFloat8_e4m3 dtype in the
420+ # generated CUDA, not a relabeled fp16 path).
421+ T .copy ( # type: ignore[name-defined] # noqa: F821
422+ A_fp8 [by * _FP8_MM_BM : by * _FP8_MM_BM + _FP8_MM_BM ,
423+ ko * _FP8_MM_BK : ko * _FP8_MM_BK + _FP8_MM_BK ],
424+ A_shared ,
425+ disable_tma = True ,
426+ )
427+ T .copy ( # type: ignore[name-defined] # noqa: F821
428+ B_fp8 [bx * _FP8_MM_BN : bx * _FP8_MM_BN + _FP8_MM_BN ,
429+ ko * _FP8_MM_BK : ko * _FP8_MM_BK + _FP8_MM_BK ],
430+ B_shared ,
431+ disable_tma = True ,
432+ )
433+ # transpose_B=True keeps the TN layout the SM120 fp8 MMA requires.
434+ T .gemm (A_shared , B_shared , C_frag , transpose_B = True ) # type: ignore[name-defined] # noqa: F821
435+ sa = A_scale [0 ]
436+ sb = B_scale [0 ]
437+ for i , j in T .Parallel (_FP8_MM_BM , _FP8_MM_BN ): # type: ignore[name-defined] # noqa: F821
438+ C_frag [i , j ] = C_frag [i , j ] * sa * sb
439+ T .copy (C_frag , C_shared ) # type: ignore[name-defined] # noqa: F821
440+ T .copy (C_shared , C [by * _FP8_MM_BM , bx * _FP8_MM_BN ], disable_tma = True ) # type: ignore[name-defined] # noqa: F821
441+
442+
300443def _make_scaled_matmul_kernel (
301444 * ,
302445 M : int ,
@@ -417,6 +560,84 @@ def _make_scaled_matmul2d_cuda_kernel(
417560 return tilelang .language .prim_func (_fp8_scaled_matmul2d_cuda_kernel_template )
418561
419562
563+ def _make_scaled_matmul2d_cuda_native_kernel (
564+ * ,
565+ M : int ,
566+ N : int ,
567+ K : int ,
568+ num_stages : int = 0 ,
569+ c_dtype : str = "float32" ,
570+ BM : int | None = None ,
571+ BN : int | None = None ,
572+ BK : int | None = None ,
573+ threads : int | None = None ,
574+ ) -> Any :
575+ """Build the gb10 (sm_121) NATIVE fp8-input e4m3 ``T.gemm`` owner-output prim.
576+
577+ Binds :func:`_fp8_scaled_matmul2d_cuda_native_kernel_template` (e4m3 shared
578+ operands fed straight into ``T.gemm`` -> SM120 fp8 MMA). Tile selection uses
579+ :func:`_native_fp8_tile_for` (K%32==0 + m16n8 divisibility), NOT the Metal
580+ cooperative selector -- the fp8 MMA wants larger BM/BN than the narrow Metal
581+ matmul2d micro-tile. An explicit (BM,BN,BK,threads) override is accepted so
582+ the microbench / a tile sweep can probe a specific geometry; the override is
583+ VALIDATED (K%32==0, divisibility, m16n8) and RAISES on an illegal tile rather
584+ than silently mis-tiling (RULE #1).
585+
586+ Returns ``None`` when no legal native-fp8 tile divides ``M/N/K`` (e.g. K not a
587+ multiple of 32). RULE #1: the caller RAISES on ``None`` -- the native fp8 MMA
588+ is the ONE path when selected; it never falls back to the fp16-decode twin or
589+ to bf16. (For an explicit override, an illegal tile RAISES here directly.)
590+ """
591+ import tilelang
592+ from tilelang import language as T
593+ from tvm .target import Target
594+
595+ override = (BM , BN , BK , threads )
596+ if any (v is not None for v in override ):
597+ if any (v is None for v in override ):
598+ raise ValueError (
599+ "fp8_scaled_matmul_path_c (cuda native): a tile override must "
600+ "specify all of BM, BN, BK, threads together (RULE #1: no "
601+ f"half-specified tile); got BM={ BM } BN={ BN } BK={ BK } threads={ threads } "
602+ )
603+ bm , bn , bk , thr = int (BM ), int (BN ), int (BK ), int (threads )
604+ # Validate the override against the fp8 MMA hard constraints (the same
605+ # gates _native_fp8_tile_for enforces). An illegal override RAISES.
606+ if bk % 32 != 0 or K % bk :
607+ raise ValueError (
608+ "fp8_scaled_matmul_path_c (cuda native): illegal BK override "
609+ f"BK={ bk } for K={ K } ; the e4m3 m16n8k32 MMA requires BK%32==0 and "
610+ "K%BK==0 (RULE #1: K%32 is a hard floor, no silent re-tile)."
611+ )
612+ if bm % 16 or bn % 8 or M % bm or N % bn :
613+ raise ValueError (
614+ "fp8_scaled_matmul_path_c (cuda native): illegal BM/BN override "
615+ f"BM={ bm } BN={ bn } for M={ M } N={ N } ; require BM%16==0, BN%8==0, "
616+ "M%BM==0, N%BN==0 (RULE #1: no wrong partial-tile)."
617+ )
618+ tile = (bm , bn , bk , thr )
619+ else :
620+ tile = _native_fp8_tile_for (int (M ), int (N ), int (K ))
621+ if tile is None :
622+ return None
623+ bm , bn , bk , thr = tile
624+
625+ globals ().update (
626+ T = T ,
627+ Target = Target ,
628+ _FP8_MM_M = int (M ),
629+ _FP8_MM_N = int (N ),
630+ _FP8_MM_K = int (K ),
631+ _FP8_MM_BM = int (bm ),
632+ _FP8_MM_BN = int (bn ),
633+ _FP8_MM_BK = int (bk ),
634+ _FP8_MM_THREADS = int (thr ),
635+ _FP8_MM_NUM_STAGES = int (num_stages ),
636+ _FP8_MM_C_DTYPE = str (c_dtype ),
637+ )
638+ return tilelang .language .prim_func (_fp8_scaled_matmul2d_cuda_native_kernel_template )
639+
640+
420641def _resolve_fp8_compile_target (target : Any ) -> Any :
421642 """Resolve the ``target`` kwarg threaded through the FP8 matmul builders.
422643
@@ -453,6 +674,34 @@ def _resolve_fp8_compile_target(target: Any) -> Any:
453674
454675FP8_PATH_C_MATMUL2D_ENV = "CPPMEGA_FP8_PATH_C_MATMUL2D"
455676
677+ # Lever r2-native-fp8mma: route the gb10 / sm_121 CUDA fp8 GEMM through the
678+ # NATIVE fp8-input e4m3 MMA prim (_fp8_scaled_matmul2d_cuda_native_kernel_template)
679+ # instead of the fp16-decode twin (_fp8_scaled_matmul2d_cuda_kernel_template).
680+ # DEFAULT: native (the real fp8-input MMA is the intended clear path on sm_121).
681+ # Set CPPMEGA_FP8_PATH_C_CUDA_NATIVE=0 to A/B against the original correct-but-slow
682+ # fp16-decode prim (e.g. to compare TFLOPs). This is an explicit A/B opt-out, NOT
683+ # a silent fallback: a native-prim compile/dispatch FAILURE still RAISES (RULE #1);
684+ # the env only chooses which CUDA prim is built, never papers over a broken one.
685+ FP8_PATH_C_CUDA_NATIVE_ENV = "CPPMEGA_FP8_PATH_C_CUDA_NATIVE"
686+
687+
688+ def _cuda_native_fp8_enabled () -> bool :
689+ """Whether the gb10 CUDA branch builds the NATIVE fp8-input MMA prim.
690+
691+ Native (real e4m3 tensor-core MMA, SM120_16x8x32_TN) is the DEFAULT clear
692+ path on sm_121 (res_r2mma.feasibility=GO, proven on-hardware). The env
693+ ``CPPMEGA_FP8_PATH_C_CUDA_NATIVE`` is an explicit A/B opt-OUT only: set it to
694+ ``0``/``false``/``off`` to build the original fp16-decode twin instead (for a
695+ side-by-side TFLOPs comparison). RULE #1: this never silently degrades -- a
696+ native-prim failure RAISES; the env merely selects which prim is compiled.
697+ """
698+ import os
699+
700+ val = os .environ .get (FP8_PATH_C_CUDA_NATIVE_ENV , "" ).strip ().lower ()
701+ if val in {"0" , "false" , "no" , "off" }:
702+ return False
703+ return True
704+
456705
457706def _matmul2d_owner_output_enabled () -> bool :
458707 """Whether to route the owner-output GEMM through Metal-4 ``matmul2d``.
@@ -535,19 +784,40 @@ def _fp8_matmul_tvm_ffi_kernel_for(
535784 # RAISES (caught + re-raised as FP8MatmulPathCDirectError by the caller); it
536785 # does NOT silently fall back to dot4, to bf16, or to a degraded precision.
537786 if "cuda" in kind :
538- cuda_prim = _make_scaled_matmul2d_cuda_kernel (
539- M = M ,
540- N = N ,
541- K = K ,
542- num_stages = num_stages ,
543- c_dtype = c_dtype ,
544- )
787+ # Lever r2-native-fp8mma: DEFAULT to the native fp8-input e4m3 MMA prim
788+ # (real SM120 tensor-core fp8 MMA). CPPMEGA_FP8_PATH_C_CUDA_NATIVE=0 opts
789+ # back to the fp16-decode twin for an explicit A/B. RULE #1: either prim's
790+ # compile/dispatch failure RAISES (re-raised as FP8MatmulPathCDirectError
791+ # by the caller); the env only selects which CUDA prim is built.
792+ if _cuda_native_fp8_enabled ():
793+ cuda_prim = _make_scaled_matmul2d_cuda_native_kernel (
794+ M = M ,
795+ N = N ,
796+ K = K ,
797+ num_stages = num_stages ,
798+ c_dtype = c_dtype ,
799+ )
800+ cuda_prim_kind = "native fp8-input e4m3 MMA (SM120_16x8x32_TN)"
801+ no_tile_detail = (
802+ "no legal native-fp8 tile divides it (the e4m3 m16n8k32 MMA "
803+ "requires K%32==0 + m16n8 divisibility)"
804+ )
805+ else :
806+ cuda_prim = _make_scaled_matmul2d_cuda_kernel (
807+ M = M ,
808+ N = N ,
809+ K = K ,
810+ num_stages = num_stages ,
811+ c_dtype = c_dtype ,
812+ )
813+ cuda_prim_kind = "fp16-decode dequant->T.gemm twin (A/B opt-out)"
814+ no_tile_detail = "no legal cooperative tile divides it"
545815 if cuda_prim is None :
546816 raise FP8MatmulPathCDirectError (
547- f"fp8_scaled_matmul_path_c (cuda): no legal cooperative tile "
548- f"divides M={ M } N={ N } K={ K } ; the CUDA dequant-> T.gemm kernel "
549- "cannot tile this shape and there is no Metal-only dot4 sibling "
550- "on CUDA (RULE #1: no silent partial-tile / fallback)."
817+ f"fp8_scaled_matmul_path_c (cuda, { cuda_prim_kind } ): "
818+ f"{ no_tile_detail } for M={ M } N={ N } K={ K } ; the CUDA fp8 T.gemm "
819+ "kernel cannot tile this shape and there is no Metal-only dot4 "
820+ "sibling on CUDA (RULE #1: no silent partial-tile / fallback)."
551821 )
552822 # sm_121 (Blackwell): disable the TMA + warp-specialized lowering so the
553823 # GEMM operand copies use plain cp.async/vectorized loads (the same
@@ -937,3 +1207,52 @@ def fp8_scaled_matmul_path_c_cuda_prim(
9371207
9381208
9391209__all__ .append ("fp8_scaled_matmul_path_c_cuda_prim" )
1210+
1211+
1212+ def fp8_scaled_matmul_path_c_cuda_native_prim (
1213+ * ,
1214+ M : int ,
1215+ N : int ,
1216+ K : int ,
1217+ num_stages : int = 0 ,
1218+ c_dtype : str = "float32" ,
1219+ BM : int | None = None ,
1220+ BN : int | None = None ,
1221+ BK : int | None = None ,
1222+ threads : int | None = None ,
1223+ ) -> Any :
1224+ """Public alias: build the gb10 (sm_121) NATIVE fp8-input e4m3 ``T.gemm`` prim.
1225+
1226+ Thin wrapper over :func:`_make_scaled_matmul2d_cuda_native_kernel` -- the
1227+ lever-r2-native-fp8mma kernel whose e4m3 shared operands feed ``T.gemm``
1228+ directly, so the CUDA backend dispatches the SM120 fp8 MMA atom
1229+ (``SM120_16x8x32_TN`` -> ``mma.sync...kind::f8f6f4.m16n8k32...e4m3.e4m3.f32``)
1230+ -- a REAL fp8-input tensor-core MMA, fp32 accumulate, NO fp16 decode.
1231+
1232+ Returns the compiled-ready ``@T.prim_func``; ``None`` when no legal native-fp8
1233+ tile divides ``M/N/K`` (the caller RAISES on a CUDA host -- RULE #1). Optional
1234+ ``BM/BN/BK/threads`` pin a specific tile for a sweep (validated; illegal ->
1235+ RAISE). Compile it via :func:`tilelang.compile` with
1236+ ``target=_as_cuda_target("cuda")`` and ``pass_configs={"tl.disable_tma_lower":
1237+ True, "tl.disable_warp_specialized": True}`` -- exactly what
1238+ :func:`_fp8_matmul_tvm_ffi_kernel_for` does on the cuda native branch.
1239+
1240+ VERIFY-GATE (RULE #1): the GB10 phase must confirm the generated CUDA emits
1241+ ``tl::mma_sync<kFloat8_e4m3,kFloat8_e4m3,kFloat32,16,8,32>`` (NOT kFloat16) --
1242+ that proves it is a real fp8-input MMA, not a relabeled fp16 path.
1243+ """
1244+ return _make_scaled_matmul2d_cuda_native_kernel (
1245+ M = M ,
1246+ N = N ,
1247+ K = K ,
1248+ num_stages = num_stages ,
1249+ c_dtype = c_dtype ,
1250+ BM = BM ,
1251+ BN = BN ,
1252+ BK = BK ,
1253+ threads = threads ,
1254+ )
1255+
1256+
1257+ __all__ .append ("fp8_scaled_matmul_path_c_cuda_native_prim" )
1258+ __all__ .append ("FP8_PATH_C_CUDA_NATIVE_ENV" )
0 commit comments