@@ -2617,6 +2617,7 @@ def _descriptor_prim_func_source(
26172617 backward_stage_selector=bool(backward_stage_name_groups),
26182618 row_dispatch_defined=False,
26192619 indent=indent,
2620+ cuda_target=(_path_c_default_target() == "cuda"),
26202621 )
26212622 else:
26222623 active_items = tuple(
@@ -4683,6 +4684,7 @@ def _append_row_phased_hidden_body(
46834684 backward_stage_selector: bool,
46844685 row_dispatch_defined: bool,
46854686 indent: str,
4687+ cuda_target: bool = False,
46864688) -> None:
46874689 if shape_env is None:
46884690 raise ValueError("row_phased_hidden loop policy requires a model shape_env")
@@ -4807,6 +4809,7 @@ def _append_row_phased_hidden_body(
48074809 thread_count=thread_count,
48084810 indent=indent,
48094811 launcher_chunked_rows=launcher_subchunked_rows,
4812+ cuda_target=cuda_target,
48104813 )
48114814 for node, descriptor, _fragment in fwd_items:
48124815 if _is_row_phased_m2rnn(node, descriptor, shape_env):
@@ -5035,6 +5038,7 @@ def append_forward_phase(lines: Sequence[str]) -> None:
50355038 thread_count=thread_count,
50365039 indent=indent,
50375040 launcher_chunked_rows=launcher_subchunked_rows,
5041+ cuda_target=cuda_target,
50385042 )
50395043 continue
50405044 if node.op_name == "residual_rmsnorm":
@@ -5700,6 +5704,7 @@ def _append_row_phased_mamba3_init(
57005704 thread_count: int,
57015705 indent: str,
57025706 launcher_chunked_rows: bool = False,
5707+ cuda_target: bool = False,
57035708) -> None:
57045709 projected = _scratch_name(node, "mamba3_projected_vec")
57055710 conv_history = _scratch_name(node, "mamba3_conv_history")
@@ -5715,6 +5720,10 @@ def _append_row_phased_mamba3_init(
57155720 trap_group = _scratch_name(node, "mamba3_trap_group")
57165721 next_dt = _scratch_name(node, "mamba3_next_dt")
57175722 next_trap = _scratch_name(node, "mamba3_next_trap")
5723+ # CUDA-only thread-parallel trapezoid scratch (declared only when
5724+ # cuda_target so the Metal kernel source is byte-for-byte unchanged).
5725+ next_dt_vec = _scratch_name(node, "mamba3_next_dt_vec")
5726+ next_trap_vec = _scratch_name(node, "mamba3_next_trap_vec")
57185727 angle_cumsum = _scratch_name(node, "mamba3_angle_cumsum")
57195728 out_inner = _scratch_name(node, "mamba3_out_inner")
57205729 accum = _scratch_name(node, "mamba3_accum")
@@ -5760,6 +5769,16 @@ def _append_row_phased_mamba3_init(
57605769 body.append(f"{indent * 2}{trap_group} = T.alloc_shared(({groups},), \"float32\")")
57615770 body.append(f"{indent * 2}{next_dt} = T.alloc_local((1,), \"float32\")")
57625771 body.append(f"{indent * 2}{next_trap} = T.alloc_local((1,), \"float32\")")
5772+ if cuda_target:
5773+ # CUDA-only: per-head next-row dt/trap projection scratch. These are in
5774+ # the force-spill list, so they alias into the ABI float32 scratch bank
5775+ # and do not consume the per-block shared-memory budget.
5776+ body.append(
5777+ f"{indent * 2}{next_dt_vec} = T.alloc_shared(({heads},), \"float32\")"
5778+ )
5779+ body.append(
5780+ f"{indent * 2}{next_trap_vec} = T.alloc_shared(({heads},), \"float32\")"
5781+ )
57635782 body.append(
57645783 f"{indent * 2}{angle_cumsum} = "
57655784 f"T.alloc_shared(({heads}, {rope_angles}), \"float32\")"
@@ -5925,6 +5944,7 @@ def _append_row_phased_mamba3_body(
59255944 thread_count: int,
59265945 indent: str,
59275946 launcher_chunked_rows: bool = False,
5947+ cuda_target: bool = False,
59285948) -> None:
59295949 projected = _scratch_name(node, "mamba3_projected_vec")
59305950 conv_history = _scratch_name(node, "mamba3_conv_history")
@@ -5940,6 +5960,12 @@ def _append_row_phased_mamba3_body(
59405960 trap_group = _scratch_name(node, "mamba3_trap_group")
59415961 next_dt = _scratch_name(node, "mamba3_next_dt")
59425962 next_trap = _scratch_name(node, "mamba3_next_trap")
5963+ # CUDA-only: per-head precomputed next-row dt/trap projections so the
5964+ # trapezoid term is computed thread-parallel over `heads` (mirrors the
5965+ # backward body), instead of serially over `groups` lanes. See the
5966+ # cuda_target branch below for why this avoids the forward runaway.
5967+ next_dt_vec = _scratch_name(node, "mamba3_next_dt_vec")
5968+ next_trap_vec = _scratch_name(node, "mamba3_next_trap_vec")
59435969 angle_cumsum = _scratch_name(node, "mamba3_angle_cumsum")
59445970 out_inner = _scratch_name(node, "mamba3_out_inner")
59455971 accum = _scratch_name(node, "mamba3_accum")
@@ -6095,6 +6121,63 @@ def _append_row_phased_mamba3_body(
60956121 f"{angle_cumsum}[{head}, {angle}] + "
60966122 f"({projected}[{angle_offset} + {angle}] * {dt_vec}[{head}])"
60976123 )
6124+ if cuda_target:
6125+ # CUDA-only: compute the next-row dt/trap raw projections for EVERY head
6126+ # in this same thread-parallel `head` loop (one head per active lane,
6127+ # `heads` lanes busy). The original (Metal) path recomputes these inside
6128+ # a `groups`-serial loop with an inner `heads_per_group` loop, so only
6129+ # `groups` lanes do the expensive `hidden`-deep reduction while the rest
6130+ # idle behind a block sync -> the forward compute-spin runaway on CUDA.
6131+ # The arithmetic is identical (same FMA order, same softplus); only the
6132+ # work distribution changes. Mirrors `_append_row_phased_mamba3_bwd_body`.
6133+ next_dt_hidden_expr = _node_indexed_positional_input_expr(
6134+ node,
6135+ 0,
6136+ dtype_by_buffer,
6137+ access_by_buffer,
6138+ f"(row + 1) * {hidden_size} + {hidden_dim_loop}",
6139+ )
6140+ next_dt_vec_weight_expr = _node_indexed_canonical_input_expr(
6141+ node,
6142+ "mamba3_in_proj_weight",
6143+ dtype_by_buffer,
6144+ access_by_buffer,
6145+ f"({dt_offset} + {head}) * {hidden_size} + {hidden_dim_loop}",
6146+ )
6147+ next_trap_vec_weight_expr = _node_indexed_canonical_input_expr(
6148+ node,
6149+ "mamba3_in_proj_weight",
6150+ dtype_by_buffer,
6151+ access_by_buffer,
6152+ f"({trap_offset} + {head}) * {hidden_size} + {hidden_dim_loop}",
6153+ )
6154+ next_dt_vec_bias = _node_indexed_canonical_input_expr(
6155+ node,
6156+ "mamba3_dt_bias",
6157+ dtype_by_buffer,
6158+ access_by_buffer,
6159+ head,
6160+ )
6161+ body.append(f"{indent * 4}{next_dt_vec}[{head}] = 0.0")
6162+ body.append(f"{indent * 4}{next_trap_vec}[{head}] = 0.0")
6163+ body.append(
6164+ f"{indent * 4}if row + 1 < {int(shape_env.sequence_length)}:"
6165+ )
6166+ body.append(
6167+ f"{indent * 5}for {hidden_dim_loop} in T.serial(0, {hidden_size}):"
6168+ )
6169+ body.append(
6170+ f"{indent * 6}{next_dt_vec}[{head}] = {next_dt_vec}[{head}] + "
6171+ f"({next_dt_hidden_expr} * {next_dt_vec_weight_expr})"
6172+ )
6173+ body.append(
6174+ f"{indent * 6}{next_trap_vec}[{head}] = {next_trap_vec}[{head}] + "
6175+ f"({next_dt_hidden_expr} * {next_trap_vec_weight_expr})"
6176+ )
6177+ body.append(
6178+ f"{indent * 5}{next_dt_vec}[{head}] = T.log(1.0 + "
6179+ f"T.exp({next_dt_vec}[{head}] + {next_dt_vec_bias}))"
6180+ )
60986181 if launcher_chunked_rows:
60996182 body.append(
61006183 f"{indent * 3}for {angle_carry_flat} in T.serial(lane, {heads * rope_angles}, "
@@ -6129,75 +6212,108 @@ def _append_row_phased_mamba3_body(
61296212 f"{angle_cumsum}[{angle_carry_head}, {angle_carry_idx}]"
61306213 )
61316214 body.append(f"{indent * 3}T.sync_threads()")
6132- body.append(
6133- f"{indent * 3}for {trap_group_loop} in T.serial(lane, {groups}, "
6134- f"step={thread_count}):"
6135- )
6136- body.append(f"{indent * 4}{trap_group}[{trap_group_loop}] = 0.0")
6137- body.append(f"{indent * 4}for {head} in T.serial(0, {heads_per_group}):")
6138- body.append(
6139- f"{indent * 5}{accum}[0] = "
6140- f"{trap_group_loop} * {heads_per_group} + {head}"
6141- )
6142- body.append(f"{indent * 5}{next_dt}[0] = 0.0")
6143- body.append(f"{indent * 5}{next_trap}[0] = 0.0")
6144- body.append(f"{indent * 5}if row + 1 < {int(shape_env.sequence_length)}:")
6145- body.append(f"{indent * 6}for {hidden_dim_loop} in T.serial(0, {hidden_size}):")
6146- next_hidden_expr = _node_indexed_positional_input_expr(
6147- node,
6148- 0,
6149- dtype_by_buffer,
6150- access_by_buffer,
6151- f"(row + 1) * {hidden_size} + {hidden_dim_loop}",
6152- )
6153- next_dt_weight_expr = _node_indexed_canonical_input_expr(
6154- node,
6155- "mamba3_in_proj_weight",
6156- dtype_by_buffer,
6157- access_by_buffer,
6158- f"({dt_offset} + T.cast({accum}[0], \"int32\")) * {hidden_size} + "
6159- f"{hidden_dim_loop}",
6160- )
6161- next_trap_weight_expr = _node_indexed_canonical_input_expr(
6162- node,
6163- "mamba3_in_proj_weight",
6164- dtype_by_buffer,
6165- access_by_buffer,
6166- f"({trap_offset} + T.cast({accum}[0], \"int32\")) * {hidden_size} + "
6167- f"{hidden_dim_loop}",
6168- )
6169- body.append(
6170- f"{indent * 7}{next_dt}[0] = {next_dt}[0] + "
6171- f"({next_hidden_expr} * {next_dt_weight_expr})"
6172- )
6173- body.append(
6174- f"{indent * 7}{next_trap}[0] = {next_trap}[0] + "
6175- f"({next_hidden_expr} * {next_trap_weight_expr})"
6176- )
6177- next_dt_bias = _node_indexed_canonical_input_expr(
6178- node,
6179- "mamba3_dt_bias",
6180- dtype_by_buffer,
6181- access_by_buffer,
6182- f'T.cast({accum}[0], "int32")',
6183- )
6184- body.append(
6185- f"{indent * 6}{next_dt}[0] = T.log(1.0 + "
6186- f"T.exp({next_dt}[0] + {next_dt_bias}))"
6187- )
6188- body.append(
6189- f"{indent * 5}{trap_group}[{trap_group_loop}] = "
6190- f"{trap_group}[{trap_group_loop}] + "
6191- f"(({next_dt}[0] * (1.0 - (1.0 / (1.0 + T.exp(-{next_trap}[0]))))) + "
6192- f"({dt_vec}[T.cast({accum}[0], \"int32\")] * "
6193- f"(1.0 / (1.0 + T.exp(-{projected}[{trap_offset} + "
6194- f"T.cast({accum}[0], \"int32\")])))))"
6195- )
6196- body.append(
6197- f"{indent * 4}{trap_group}[{trap_group_loop}] = "
6198- f"{trap_group}[{trap_group_loop}] / "
6199- f"{float(heads_per_group):.1f}"
6200- )
6215+ if cuda_target:
6216+ # CUDA-only: the next-row dt/trap reductions were already done
6217+ # thread-parallel over `heads` above. This per-group reduction now just
6218+ # sums the precomputed `next_dt_vec`/`next_trap_vec` (no `hidden`-deep
6219+ # inner loop), so the whole trapezoid phase is cheap and no longer
6220+ # serializes the block on `groups` lanes. Same arithmetic as the Metal
6221+ # path below.
6222+ body.append(
6223+ f"{indent * 3}for {trap_group_loop} in T.serial(lane, {groups}, "
6224+ f"step={thread_count}):"
6225+ )
6226+ body.append(f"{indent * 4}{trap_group}[{trap_group_loop}] = 0.0")
6227+ body.append(f"{indent * 4}for {head} in T.serial(0, {heads_per_group}):")
6228+ body.append(
6229+ f"{indent * 5}{accum}[0] = "
6230+ f"{trap_group_loop} * {heads_per_group} + {head}"
6231+ )
6232+ body.append(
6233+ f"{indent * 5}{trap_group}[{trap_group_loop}] = "
6234+ f"{trap_group}[{trap_group_loop}] + "
6235+ f"(({next_dt_vec}[T.cast({accum}[0], \"int32\")] * "
6236+ f"(1.0 - (1.0 / (1.0 + T.exp(-{next_trap_vec}["
6237+ f"T.cast({accum}[0], \"int32\")]))))) + "
6238+ f"({dt_vec}[T.cast({accum}[0], \"int32\")] * "
6239+ f"(1.0 / (1.0 + T.exp(-{projected}[{trap_offset} + "
6240+ f"T.cast({accum}[0], \"int32\")])))))"
6241+ )
6242+ body.append(
6243+ f"{indent * 4}{trap_group}[{trap_group_loop}] = "
6244+ f"{trap_group}[{trap_group_loop}] / "
6245+ f"{float(heads_per_group):.1f}"
6246+ )
6247+ else:
6248+ body.append(
6249+ f"{indent * 3}for {trap_group_loop} in T.serial(lane, {groups}, "
6250+ f"step={thread_count}):"
6251+ )
6252+ body.append(f"{indent * 4}{trap_group}[{trap_group_loop}] = 0.0")
6253+ body.append(f"{indent * 4}for {head} in T.serial(0, {heads_per_group}):")
6254+ body.append(
6255+ f"{indent * 5}{accum}[0] = "
6256+ f"{trap_group_loop} * {heads_per_group} + {head}"
6257+ )
6258+ body.append(f"{indent * 5}{next_dt}[0] = 0.0")
6259+ body.append(f"{indent * 5}{next_trap}[0] = 0.0")
6260+ body.append(f"{indent * 5}if row + 1 < {int(shape_env.sequence_length)}:")
6261+ body.append(f"{indent * 6}for {hidden_dim_loop} in T.serial(0, {hidden_size}):")
6262+ next_hidden_expr = _node_indexed_positional_input_expr(
6263+ node,
6264+ 0,
6265+ dtype_by_buffer,
6266+ access_by_buffer,
6267+ f"(row + 1) * {hidden_size} + {hidden_dim_loop}",
6268+ )
6269+ next_dt_weight_expr = _node_indexed_canonical_input_expr(
6270+ node,
6271+ "mamba3_in_proj_weight",
6272+ dtype_by_buffer,
6273+ access_by_buffer,
6274+ f"({dt_offset} + T.cast({accum}[0], \"int32\")) * {hidden_size} + "
6275+ f"{hidden_dim_loop}",
6276+ )
6277+ next_trap_weight_expr = _node_indexed_canonical_input_expr(
6278+ node,
6279+ "mamba3_in_proj_weight",
6280+ dtype_by_buffer,
6281+ access_by_buffer,
6282+ f"({trap_offset} + T.cast({accum}[0], \"int32\")) * {hidden_size} + "
6283+ f"{hidden_dim_loop}",
6284+ )
6285+ body.append(
6286+ f"{indent * 7}{next_dt}[0] = {next_dt}[0] + "
6287+ f"({next_hidden_expr} * {next_dt_weight_expr})"
6288+ )
6289+ body.append(
6290+ f"{indent * 7}{next_trap}[0] = {next_trap}[0] + "
6291+ f"({next_hidden_expr} * {next_trap_weight_expr})"
6292+ )
6293+ next_dt_bias = _node_indexed_canonical_input_expr(
6294+ node,
6295+ "mamba3_dt_bias",
6296+ dtype_by_buffer,
6297+ access_by_buffer,
6298+ f'T.cast({accum}[0], "int32")',
6299+ )
6300+ body.append(
6301+ f"{indent * 6}{next_dt}[0] = T.log(1.0 + "
6302+ f"T.exp({next_dt}[0] + {next_dt_bias}))"
6303+ )
6304+ body.append(
6305+ f"{indent * 5}{trap_group}[{trap_group_loop}] = "
6306+ f"{trap_group}[{trap_group_loop}] + "
6307+ f"(({next_dt}[0] * (1.0 - (1.0 / (1.0 + T.exp(-{next_trap}[0]))))) + "
6308+ f"({dt_vec}[T.cast({accum}[0], \"int32\")] * "
6309+ f"(1.0 / (1.0 + T.exp(-{projected}[{trap_offset} + "
6310+ f"T.cast({accum}[0], \"int32\")])))))"
6311+ )
6312+ body.append(
6313+ f"{indent * 4}{trap_group}[{trap_group_loop}] = "
6314+ f"{trap_group}[{trap_group_loop}] / "
6315+ f"{float(heads_per_group):.1f}"
6316+ )
62016317 body.append(f"{indent * 3}T.sync_threads()")
62026318 body.append(f"{indent * 3}# mamba3_bc_policy: rank_group_rmsnorm_rope")
62036319 body.append(
0 commit comments