2121The production owner-output API is
2222``fp8_scaled_vecmat_path_c(..., out=existing_array)``: it compiles a tvm-ffi
2323kernel and passes the caller-owned MLX buffers through DLPack without building
24- ``mx.fast.metal_kernel`` or allocating/casting the output in Python. The
25- historical no-``out`` API is retired because it could only be implemented by
26- allocating an output through MLX's direct-MSL wrapper.
24+ ``mx.fast.metal_kernel`` or allocating/casting the output in Python. It returns
25+ the MLX graph output that aliases the caller-owned buffer; evaluating that
26+ returned value schedules the write, and the caller-owned ``out`` observes the
27+ same storage afterward. The historical no-``out`` API is retired because it
28+ could only be implemented by allocating an output through MLX's direct-MSL
29+ wrapper.
2730"""
2831
2932from __future__ import annotations
6164# toggled by the env var ``TILELANG_DISABLE_FP8_DOT4_AUTO``; we don't
6265# override that env var here.
6366_FP8_VECMAT_PATH_C_CANDIDATE_PASS_CONFIGS : dict [str , Any ] = {
64- # The current Metal scalar/CSE pipeline can produce an invalid one-element
65- # SeqStmt around the canonical C[col] write in this reducer. This kernel
66- # does not need CSE: diagnostic source inspection fuses in the canonical
67- # packed hot loop, and the owner-output route already emits one
68- # dot4/simd_sum body. Keep the gate per-kernel rather than disabling CSE
69- # globally.
70- "tirx.disable_cse_tir" : True ,
67+ # MLX native TVM-FFI already performs static ABI shape/dtype/layout checks
68+ # when binding DLTensor views. The generated TVM C-host wrapper otherwise
69+ # repeats that validation on every tiny GEMV launch and dominates runtime.
70+ "tirx.disable_assert" : True ,
7171 # Z3 idea #4 — discharges ``if (i < N)`` guards the analyzer can prove.
72- # The M=1 vecmat hot loop has tight static extents so the prover tends
73- # to succeed; if not, the guard stays.
72+ # The M=1 vecmat hot loop has tight static extents. Leave CSE enabled so
73+ # TileLang hoists the global row and dot4 word index instead of repeating
74+ # grid/thread arithmetic in every FP8 dot4 iteration.
7475 "tl.drop_provable_bound_checks" : True ,
7576}
7677
@@ -461,15 +462,10 @@ def fp8_vecmat_reduce(
461462 C [col ] = reduced [0 ] * A_scale [0 ] * B_scale [col ]
462463
463464 elif _uses_fp8_dot4_packed_macro (vec = vec , K = K ):
464- # Fix-1 + Fix-A re-application: ensure the Path C Metal FP8 ops
465- # (notably ``tirx.metal.fp8_e4m3_dot4``) are registered before we
466- # parse the macro PrimFunc. Without this we get an opaque FFI
467- # ``AttributeError`` deep in the lowering pipeline; with it we get
468- # a clear ``RuntimeError`` naming the missing intrinsic.
469- # grok design P2: cache the result so we run the registration
470- # scan once per process instead of on every kernel build. Failure
471- # still raises (the intrinsic is required for correctness); only
472- # the *successful* check is cached.
465+ # Keep the diagnostic reducer on the public TileLang intrinsic surface.
466+ # The actual dot4/simd_sum shape is now selected by TileLang's
467+ # FP8 marker late-lowerer rather than by spelling Metal intrinsics in
468+ # cppmega's Python IR builder.
473469 _ensure_path_c_metal_fp8_intrinsics_registered ()
474470
475471 @T .prim_func
@@ -483,32 +479,18 @@ def fp8_vecmat_reduce(
483479 with T .Kernel (
484480 T .ceildiv (_FP8_VM_N , _FP8_VM_NP ), threads = _FP8_VM_RT * _FP8_VM_NP
485481 ) as bx :
486- accum = T .alloc_local ((1 ,), "float32" )
487- lane = T .get_thread_binding (0 )
488- kr = T .floormod (lane , _FP8_VM_RT )
489- ni = T .floordiv (lane , _FP8_VM_RT )
490- col = bx * _FP8_VM_NP + ni
491- T .clear (accum )
492- for ko in T .unroll (
493- 0 ,
494- T .ceildiv (_FP8_VM_K_WORDS , _FP8_VM_RT ),
495- explicit = False ,
496- unroll_factor = 4 ,
497- ):
498- i = ko * _FP8_VM_RT + kr
499- if col < _FP8_VM_N and i < _FP8_VM_K_WORDS :
500- accum [0 ] += T .metal_fp8_e4m3_dot4 (
501- T .access_ptr (A [0 , 0 ], "r" , extent = _FP8_VM_K ),
502- T .access_ptr (B [col , 0 ], "r" , extent = _FP8_VM_K ),
503- i ,
504- i ,
505- )
506- reduced = T .call_intrin ("float32" , "tir.metal.simd_sum" , accum [0 ])
507- if kr == 0 and col < _FP8_VM_N :
508- if _FP8_VM_SW == 1 :
509- C [0 , col ] = reduced * A_scale [0 ] * B_scale [0 ]
510- else :
511- C [0 , col ] = reduced * A_scale [0 ] * B_scale [col ]
482+ T .fp8_scaled_matmul (
483+ A ,
484+ A_scale ,
485+ B ,
486+ B_scale ,
487+ C ,
488+ transpose_B = True ,
489+ c_col_offset = bx * _FP8_VM_NP ,
490+ simd_group_width = _FP8_VM_RT ,
491+ outputs_per_block = _FP8_VM_NP ,
492+ accumulate = False ,
493+ )
512494
513495 else :
514496
@@ -654,15 +636,6 @@ def lower_fp8_vecmat_msl(
654636 pass_configs = pass_configs or None ,
655637 ),
656638 )
657- lowering , _output_shape = _fuse_canonical_vecmat_runtime_body (
658- lowering ,
659- N = N ,
660- K = K ,
661- reduce_threads = reduce_threads ,
662- vec = vec ,
663- scale_w_per_row = scale_w_per_row ,
664- vectorized_loads = vectorized_loads ,
665- )
666639 return lowering .msl_text
667640
668641
@@ -704,7 +677,9 @@ def fp8_vecmat_msl_features(msl: str) -> dict[str, int]:
704677 "uint_pointer" : body .count ("uint*" ),
705678 "uchar4" : body_lowered .count ("uchar4" ),
706679 "fp8_e4m3_lut" : body .count ("fp8_e4m3fn_lut" ),
707- "metal_fp8_dot4_helper" : body .count ("__tvm_fp8_e4m3_dot4_packed" ),
680+ "metal_fp8_dot4_helper" : body .count ("__tvm_fp8_e4m3_dot4_packed" )
681+ + body .count ("__tvm_fp8_e4m3_dot4_words" )
682+ + body .count ("__tvm_fp8_e4m3fn_lut[" ),
708683 "packed_uint_loads" : packed_uint_loads ,
709684 "scalar_fp8_byte_decode" : scalar_decode_sites ,
710685 "scalar_fp8_byte_decode_calls" : scalar_decode_sites ,
@@ -758,12 +733,13 @@ def _make_fp8_vecmat_direct_kernel(
758733
759734 The ABI is intentionally flat:
760735
761- * ``A`` is the existing ``(K,)`` MLX uint8/e4m3 buffer.
736+ * ``A`` is the existing ``(K,)`` MLX uint8/e4m3 buffer, viewed by the
737+ TileLang ABI as ``(1, K)``.
762738 * ``B`` is the existing ``(N, K)`` MLX uint8/e4m3 buffer.
763739 * ``C`` is the caller-owned ``(N,)`` output buffer.
764740
765741 No reshape, allocation, or Python-side cast is needed before the DLPack
766- handoff.
742+ handoff; the tvm-ffi adapter carries the ABI shape separately .
767743 """
768744
769745 _validate_shape (
@@ -798,42 +774,28 @@ def _make_fp8_vecmat_direct_kernel(
798774
799775 @T .prim_func
800776 def fp8_vecmat_reduce_direct (
801- A : T .Tensor ((_FP8_VM_K , ), "float8_e4m3" ),
777+ A : T .Tensor ((1 , _FP8_VM_K ), "float8_e4m3" ),
802778 A_scale : T .Tensor ((1 ,), "float32" ),
803779 B : T .Tensor ((_FP8_VM_N , _FP8_VM_K ), "float8_e4m3" ),
804780 B_scale : T .Tensor ((_FP8_VM_SW ,), "float32" ),
805- C : T .Tensor ((_FP8_VM_N , ), _FP8_VM_C_DTYPE ),
781+ C : T .Tensor ((1 , _FP8_VM_N ), _FP8_VM_C_DTYPE ),
806782 ):
807783 with T .Kernel (
808784 T .ceildiv (_FP8_VM_N , _FP8_VM_NP ),
809785 threads = _FP8_VM_RT * _FP8_VM_NP ,
810786 ) as bx :
811- accum = T .alloc_local ((1 ,), "float32" )
812- lane = T .get_thread_binding (0 )
813- kr = T .floormod (lane , _FP8_VM_RT )
814- ni = T .floordiv (lane , _FP8_VM_RT )
815- col = bx * _FP8_VM_NP + ni
816- T .clear (accum )
817- for ko in T .unroll (
818- 0 ,
819- T .ceildiv (_FP8_VM_K_WORDS , _FP8_VM_RT ),
820- explicit = False ,
821- unroll_factor = 4 ,
822- ):
823- word_i = ko * _FP8_VM_RT + kr
824- if col < _FP8_VM_N and word_i < _FP8_VM_K_WORDS :
825- accum [0 ] += T .metal_fp8_e4m3_dot4 (
826- T .access_ptr (A [0 ], "r" , extent = _FP8_VM_K ),
827- T .access_ptr (B [col , 0 ], "r" , extent = _FP8_VM_K ),
828- word_i ,
829- word_i ,
830- )
831- reduced = T .call_intrin ("float32" , "tir.metal.simd_sum" , accum [0 ])
832- if kr == 0 and col < _FP8_VM_N :
833- if _FP8_VM_SW == 1 :
834- C [col ] = reduced * A_scale [0 ] * B_scale [0 ]
835- else :
836- C [col ] = reduced * A_scale [0 ] * B_scale [col ]
787+ T .fp8_scaled_matmul (
788+ A ,
789+ A_scale ,
790+ B ,
791+ B_scale ,
792+ C ,
793+ transpose_B = True ,
794+ c_col_offset = bx * _FP8_VM_NP ,
795+ simd_group_width = _FP8_VM_RT ,
796+ outputs_per_block = _FP8_VM_NP ,
797+ accumulate = False ,
798+ )
837799
838800 try :
839801 from tilelang .transform .simplify import apply_simplify
@@ -1120,7 +1082,14 @@ def fp8_scaled_vecmat_path_c_direct(
11201082 ) from exc
11211083
11221084 try :
1123- returned = kernel (A , A_scale , B , B_scale , C )
1085+ returned = kernel (
1086+ A ,
1087+ A_scale ,
1088+ B ,
1089+ B_scale ,
1090+ C ,
1091+ _tilelang_mlx_async_owner_outputs = True ,
1092+ )
11241093 except Exception as exc :
11251094 try :
11261095 from tilelang .contrib .mlx_interop import DLPackInteropError
@@ -1131,11 +1100,7 @@ def fp8_scaled_vecmat_path_c_direct(
11311100 raise FP8VecmatPathCDirectError (
11321101 f"direct tvm-ffi FP8 vecmat dispatch failed: { type (exc ).__name__ } : { exc } "
11331102 ) from exc
1134- if returned is not C :
1135- raise FP8VecmatPathCDirectError (
1136- "direct tvm-ffi FP8 vecmat did not return the caller-owned output"
1137- )
1138- return C
1103+ return returned
11391104
11401105
11411106def fp8_scaled_vecmat_path_c (
@@ -1154,9 +1119,10 @@ def fp8_scaled_vecmat_path_c(
11541119 ``x_fp8`` is ``(K,)`` uint8 e4m3 storage and ``W_fp8`` is transposed
11551120 ``(N, K)`` storage, matching Path B. ``scale_x`` is scalar; ``scale_w`` may
11561121 be scalar or per-output ``(N,)``. When ``out`` is provided, dispatches via
1157- tvm-ffi into that caller-owned output and returns the same object. Without
1158- ``out``, this function fails explicitly: there is no non-owner-output Path C
1159- dispatch surface.
1122+ tvm-ffi into that caller-owned output and returns the MLX graph output that
1123+ aliases ``out``. Evaluating the returned value schedules the write; reading
1124+ ``out`` after that observes the same storage. Without ``out``, this function
1125+ fails explicitly: there is no non-owner-output Path C dispatch surface.
11601126 """
11611127
11621128 if out is not None :
0 commit comments