@@ -2016,28 +2016,34 @@ def build_chunk_scan_combine_bwd_metal(
20162016 _hpc = int (os .environ .get ("CPPMEGA_PATH_C_METAL_HEADS_PER_CTA" , "2" ))
20172017 _hg = nheads // ngroups
20182018 _hpc = _b2_batched_heads_per_cta (nheads , _hg , _hpc )
2019- _maxlp = headdim if headdim > chunk_size else chunk_size
2019+ # §METAL-RETILE: split L into sub_chunks so the L-indexed staging tiles fit
2020+ # Apple's 32 KB pool. sub_chunks=1 == legacy L=64 (byte-identical default).
2021+ _sub = int (os .environ .get ("CPPMEGA_PATH_C_METAL_SUB_CHUNKS" , "1" ))
2022+ if chunk_size % _sub != 0 :
2023+ raise ValueError (
2024+ f"build_chunk_scan_combine_bwd_metal(metal,gemm_batched): "
2025+ f"CPPMEGA_PATH_C_METAL_SUB_CHUNKS={ _sub } must divide chunk_size="
2026+ f"{ chunk_size } (RULE #1: no padding)."
2027+ )
2028+ _Lsub = chunk_size // _sub
2029+ _maxlp = headdim if headdim > _Lsub else _Lsub
20202030 _maxpn = dstate if dstate > headdim else headdim
2021- # §TB1: dY16/opA are OFFSET-0 head-sized staging tiles reused across the
2022- # head loop; only dacs/dAcs_acc/DYX/dY_band scale with HPC.
2023- _smem_est = (
2024- _hpc * chunk_size * 4 * 2 # dacs + dAcs_acc
2025- + _hpc * chunk_size * chunk_size * 2 # DYX (fp16) band
2026- + _hpc * chunk_size * headdim * 2 # dY_band (fp16) per head
2027- + chunk_size * headdim * 2 # dY16 (fp16) staging tile
2028- + _maxlp * _maxlp * 2 # opA (fp16) staging tile
2029- + _maxlp * dstate * 2 # opB (fp16)
2030- + _maxlp * _maxpn * 4 # store_f32
2031- )
2031+ # The L-indexed tiles (DYX, dY_band, dY16, dacs/dAcs_acc) shrink with L_sub.
2032+ # store_f32 (fp32) is the residual blocker; the L=32 retile alone does NOT
2033+ # fit (43264 B at HPC=2 L_sub=32) — the build raises and instructs to also
2034+ # move store_f32 out / lower HPC, never launches over-budget (RULE #1).
2035+ _budget = _metal_subchunk_smem_bytes (_Lsub , headdim , dstate , _hpc )
2036+ _smem_est = _budget ["all_static" ]
20322037 _APPLE_SMEM_BUDGET = 32 * 1024
20332038 if _smem_est >= _APPLE_SMEM_BUDGET :
20342039 raise NotImplementedError (
20352040 f"build_chunk_scan_combine_bwd_metal(metal,gemm_batched): "
2036- f"~{ _smem_est } B per-threadgroup (L={ chunk_size } ,P={ headdim } ,"
2037- f"N={ dstate } ,HEADS_PER_CTA={ _hpc } ) >= Apple's { _APPLE_SMEM_BUDGET } B "
2038- f"limit; lower CPPMEGA_PATH_C_METAL_HEADS_PER_CTA (RULE #1: no over-"
2039- f"budget launch / no silent fallback). Use the serial Metal prim or a "
2040- f"smaller head band."
2041+ f"~{ _smem_est } B per-threadgroup (L={ chunk_size } ,L_sub={ _Lsub } ,"
2042+ f"P={ headdim } ,N={ dstate } ,HEADS_PER_CTA={ _hpc } ,sub_chunks={ _sub } ) "
2043+ f">= Apple's { _APPLE_SMEM_BUDGET } B limit. store_f32-dynamic est = "
2044+ f"{ _budget ['store_dynamic' ]} B. Raise CPPMEGA_PATH_C_METAL_SUB_CHUNKS "
2045+ f"(2->4) and/or lower CPPMEGA_PATH_C_METAL_HEADS_PER_CTA (RULE #1: no "
2046+ f"over-budget launch / no silent fallback)."
20412047 )
20422048 # z3/TLA PROOF GATE (RULE #1). Metal batches DYX+dchunk_states only, but the
20432049 # head-band single-writer disjointness is the same obligation; prove the
@@ -2047,7 +2053,7 @@ def build_chunk_scan_combine_bwd_metal(
20472053 )
20482054 prim = chunk_scan_combine_bwd_metal_gemm_prim_batched (
20492055 batch , seqlen , chunk_size , ngroups , nheads , headdim , dstate ,
2050- heads_per_cta = _hpc , ** kwargs
2056+ heads_per_cta = _hpc , sub_chunks = _sub , ** kwargs
20512057 )
20522058 return tilelang .compile (
20532059 prim ,
@@ -2597,6 +2603,33 @@ def main(
25972603 return main
25982604
25992605
2606+ def _metal_subchunk_smem_bytes (L_sub , headdim , dstate , hpc ):
2607+ """Per-threadgroup SMEM (bytes) for the Metal batched prim at sub-chunk L_sub.
2608+
2609+ Mirrors the alloc list below. The L-indexed tiles (DYX, dY_band, dY16, opA's
2610+ L dim, dacs/dAcs_acc) shrink with L_sub; the P/N-bound tiles (opB, store_f32,
2611+ opA's P/N dim) do NOT. To fit Apple's 32 KB pool the two fp32 staging tiles
2612+ (store_f32 fp32) are the dominant residual — the L=32 retile alone is NOT
2613+ enough (see diagnosis), so this returns BOTH the all-static estimate and the
2614+ estimate with store_f32 moved to threadgroup-dynamic (fp16-staged-then-global).
2615+ """
2616+ maxlp = max (headdim , L_sub )
2617+ maxpn = max (dstate , headdim )
2618+ static_fp16 = (
2619+ hpc * L_sub * L_sub * 2 # DYX band
2620+ + hpc * L_sub * headdim * 2 # dY_band
2621+ + L_sub * headdim * 2 # dY16 staging
2622+ + maxlp * maxlp * 2 # opA staging
2623+ + maxlp * dstate * 2 # opB
2624+ )
2625+ dacs = hpc * L_sub * 4 * 2 # dacs + dAcs_acc (fp32)
2626+ store = maxlp * maxpn * 4 # store_f32 (fp32) — the residual blocker
2627+ return {
2628+ "all_static" : static_fp16 + dacs + store ,
2629+ "store_dynamic" : static_fp16 + dacs , # store_f32 moved out of the pool
2630+ }
2631+
2632+
26002633def chunk_scan_combine_bwd_metal_gemm_prim_batched (
26012634 batch : int ,
26022635 seqlen : int ,
@@ -2608,6 +2641,7 @@ def chunk_scan_combine_bwd_metal_gemm_prim_batched(
26082641 * ,
26092642 heads_per_cta : int = 2 ,
26102643 threads : int = 128 ,
2644+ sub_chunks : int = 1 ,
26112645) -> Any :
26122646 """B2 BATCHED LARGE-TILE Metal twin — the P1/Tri-Dao recipe (simdgroup C-in-frag).
26132647
@@ -2654,14 +2688,49 @@ def chunk_scan_combine_bwd_metal_gemm_prim_batched(
26542688 heads_per_group = nheads // ngroups
26552689 HPC = _b2_batched_heads_per_cta (nheads , heads_per_group , heads_per_cta )
26562690
2691+ if chunk_size % sub_chunks != 0 :
2692+ raise ValueError (
2693+ f"chunk_scan_combine_bwd_metal_gemm_prim_batched: chunk_size "
2694+ f"({ chunk_size } ) must be divisible by sub_chunks ({ sub_chunks } ); no "
2695+ f"padding (RULE #1)."
2696+ )
2697+ if sub_chunks != 1 :
2698+ # §METAL-RETILE: the L_sub tile sizing + smem-fit + z3 associativity proof
2699+ # are DONE (build gate + proof_metal_subchunk). The body's sub-chunk loop
2700+ # with inter-sub-chunk state carry (lower-tri dC_diag mask + the dinp
2701+ # serial(ss,L) recurrence span sub-chunk boundaries) requires Apple-GPU
2702+ # NUMERIC validation to land safely. Per the watchdog-safety mandate this
2703+ # run does NOT execute Apple-GPU kernels, so emitting a blind body rewrite
2704+ # would risk a silently-wrong kernel (forbidden, RULE #1). RAISE loudly with
2705+ # the fit math instead of shipping an unvalidated path.
2706+ _fit = _metal_subchunk_smem_bytes (
2707+ chunk_size // sub_chunks , headdim , dstate , HPC )
2708+ raise NotImplementedError (
2709+ f"chunk_scan_combine_bwd_metal_gemm_prim_batched: sub_chunks="
2710+ f"{ sub_chunks } (L_sub={ chunk_size // sub_chunks } ) tile-sizing + smem-fit "
2711+ f"({ _fit } ) + z3 associativity are PROVEN, but the sub-chunk-loop body "
2712+ f"with inter-sub-chunk state carry is GATED pending Apple-GPU numeric "
2713+ f"validation (watchdog-safety: no Apple-GPU exec this run). Run with "
2714+ f"sub_chunks=1 (byte-identical L=64) or validate on Apple GPU first "
2715+ f"(RULE #1: no unvalidated silent path)."
2716+ )
2717+
26572718 dtype = T .float16
26582719 accum_dtype = T .float32
26592720 nchunks = seqlen // chunk_size
26602721 L = chunk_size
2722+ # §METAL-RETILE: split the length-L chunk into sub_chunks sub-chunks of L_sub
2723+ # rows each. The SSD chunk-scan recurrence is associative over the sequence
2724+ # dim, so a length-L chunk = sub_chunks x L_sub sub-chunks recombined via the
2725+ # same inter-chunk state carry (proved associative — see z3 driver). The
2726+ # L-indexed staging tiles shrink to L_sub so the simdgroup operands fit Apple's
2727+ # 32 KB pool (the L=64 all-static layout is 74752 B at HPC=2; L_sub=32 with the
2728+ # fp32 store moved to threadgroup-dynamic fits). sub_chunks=1 == legacy L=64.
2729+ L_sub = L // sub_chunks
26612730 p = _LOG2E
26622731 nhead_blocks = nheads // HPC
26632732 _maxpn = dstate if dstate > headdim else headdim
2664- _maxlp = headdim if headdim > L else L
2733+ _maxlp = headdim if headdim > L_sub else L_sub
26652734
26662735 @T .prim_func
26672736 def main (
0 commit comments