5252 write_into_bank_slot ,
5353)
5454from cppmega_mlx .runtime .path_c_taps import emit_and_tap_path_c_tensor
55+ from cppmega_mlx .training .cut_cross_entropy import (
56+ DEFAULT_CHUNK_ROWS ,
57+ linear_cross_entropy ,
58+ )
5559from cppmega_mlx .training .mtp import MinimalMTPHead , MTPLossConfig
5660
5761if TYPE_CHECKING :
@@ -1175,6 +1179,92 @@ def path_c_fused_train_block_prim_func(
11751179 cache [sequence_length ] = result
11761180 return result
11771181
1182+ def path_c_fused_train_block_stage_prim_funcs (
1183+ self ,
1184+ * ,
1185+ sequence_length : int | None = None ,
1186+ ) -> tuple [Any , ...]:
1187+ """Return descriptor-planned generated stage PrimFuncs for this block."""
1188+
1189+ cache : dict [int | None , tuple [Any , ...]] = getattr (
1190+ self ,
1191+ "_stage_prim_func_cache" ,
1192+ None ,
1193+ ) or {}
1194+ if not hasattr (self , "_stage_prim_func_cache" ):
1195+ self ._stage_prim_func_cache = cache
1196+ if sequence_length in cache :
1197+ return cache [sequence_length ]
1198+
1199+ abi_prim_func = self .path_c_fused_train_block_prim_func (
1200+ sequence_length = sequence_length ,
1201+ )
1202+ if abi_prim_func is None :
1203+ cache [sequence_length ] = ()
1204+ return ()
1205+
1206+ from cppmega_mlx .runtime .path_c_fusion_schedules import (
1207+ path_c_descriptor_stage_prim_funcs ,
1208+ plan_path_c_fusion_schedule_for_region ,
1209+ )
1210+
1211+ regions = self .path_c_fusion_regions (
1212+ include_backward = False ,
1213+ sequence_length = sequence_length ,
1214+ )
1215+ if not regions :
1216+ cache [sequence_length ] = ()
1217+ return ()
1218+ selected = max (
1219+ regions ,
1220+ key = lambda region : (
1221+ len (region .nodes ),
1222+ len (region .edges ),
1223+ region .name ,
1224+ ),
1225+ )
1226+ planned = plan_path_c_fusion_schedule_for_region (
1227+ selected ,
1228+ include_backward = True ,
1229+ )
1230+ schedule_target = getattr (planned , "schedule_target" , None )
1231+ if schedule_target is None :
1232+ cache [sequence_length ] = ()
1233+ return ()
1234+ stage_prim_funcs = path_c_descriptor_stage_prim_funcs (
1235+ region = planned .region ,
1236+ schedule_target = schedule_target ,
1237+ abi_prim_func = abi_prim_func ,
1238+ )
1239+ cache [sequence_length ] = stage_prim_funcs
1240+ return stage_prim_funcs
1241+
1242+ def path_c_fused_train_block_physical_abi (
1243+ self ,
1244+ * ,
1245+ sequence_length : int | None = None ,
1246+ ) -> tuple [dict [str , Any ], dict [str , tuple [int , ...]], tuple [Any , ...]]:
1247+ """Return the merged physical ABI used by all generated train stages."""
1248+
1249+ prim_func = self .path_c_fused_train_block_prim_func (
1250+ sequence_length = sequence_length ,
1251+ )
1252+ if prim_func is None :
1253+ return {}, {}, ()
1254+
1255+ from cppmega_mlx .runtime .path_c_fusion_schedules import (
1256+ merge_path_c_physical_abi_for_prim_funcs ,
1257+ )
1258+
1259+ prim_funcs = (
1260+ prim_func ,
1261+ * self .path_c_fused_train_block_stage_prim_funcs (
1262+ sequence_length = sequence_length ,
1263+ ),
1264+ )
1265+ abi_map , abi_shapes = merge_path_c_physical_abi_for_prim_funcs (prim_funcs )
1266+ return abi_map , abi_shapes , prim_funcs
1267+
11781268 def make_path_c_physical_abi_bank_owner (
11791269 self ,
11801270 * ,
@@ -1191,19 +1281,9 @@ def make_path_c_physical_abi_bank_owner(
11911281
11921282 Returns ``None`` when no Path C route region exists for the model.
11931283 """
1194- prim_func = self .path_c_fused_train_block_prim_func (
1284+ abi_map , abi_shapes , prim_funcs = self .path_c_fused_train_block_physical_abi (
11951285 sequence_length = sequence_length ,
11961286 )
1197- if prim_func is None :
1198- return None
1199- abi_map = dict (
1200- getattr (prim_func , "_cppmega_path_c_physical_buffer_abi_map" , {})
1201- or {}
1202- )
1203- abi_shapes = dict (
1204- getattr (prim_func , "_cppmega_path_c_physical_buffer_abi_shapes" , {})
1205- or {}
1206- )
12071287 if not abi_map or not abi_shapes :
12081288 return None
12091289 bank_dtypes : dict [str , str ] = {}
@@ -1228,11 +1308,20 @@ def make_path_c_physical_abi_bank_owner(
12281308 tuple (int (dim ) for dim in tuple (shape )),
12291309 dtype = mx_dtype ,
12301310 )
1231- for name , spec in path_c_top_level_kernel_buffer_specs (
1232- prim_func ,
1233- physical_abi_map = abi_map ,
1234- physical_abi_shapes = abi_shapes ,
1235- ).items ():
1311+ top_level_specs : dict [str , dict [str , Any ]] = {}
1312+ for prim_func in prim_funcs :
1313+ for name , spec in path_c_top_level_kernel_buffer_specs (
1314+ prim_func ,
1315+ physical_abi_map = abi_map ,
1316+ physical_abi_shapes = abi_shapes ,
1317+ ).items ():
1318+ existing = top_level_specs .get (name )
1319+ shape = tuple (int (dim ) for dim in tuple (spec ["shape" ]))
1320+ if existing is None or math .prod (shape ) > math .prod (
1321+ tuple (existing ["shape" ])
1322+ ):
1323+ top_level_specs [name ] = dict (spec )
1324+ for name , spec in top_level_specs .items ():
12361325 if name in bank_buffers :
12371326 continue
12381327 bank_buffers [name ] = mx .zeros (
@@ -1276,15 +1365,9 @@ def path_c_fused_in_region_parameter_bank_aliases(
12761365 no ``*_grad`` slot (for example pure inputs) are skipped.
12771366 """
12781367
1279- prim_func = self .path_c_fused_train_block_prim_func (
1368+ abi_map , _abi_shapes , _prim_funcs = self .path_c_fused_train_block_physical_abi (
12801369 sequence_length = sequence_length ,
12811370 )
1282- if prim_func is None :
1283- return {}
1284- abi_map = dict (
1285- getattr (prim_func , "_cppmega_path_c_physical_buffer_abi_map" , {})
1286- or {}
1287- )
12881371 if not abi_map :
12891372 return {}
12901373 grad_logical_names = frozenset (
@@ -1468,13 +1551,9 @@ def sync_path_c_in_region_parameters_into_bank(
14681551 "synced" : [],
14691552 "skipped" : [],
14701553 }
1471- prim_func = self .path_c_fused_train_block_prim_func (
1554+ abi_map , _abi_shapes , _prim_funcs = self .path_c_fused_train_block_physical_abi (
14721555 sequence_length = sequence_length ,
14731556 )
1474- abi_map = dict (
1475- getattr (prim_func , "_cppmega_path_c_physical_buffer_abi_map" , {})
1476- or {}
1477- )
14781557 synced : list [str ] = []
14791558 skipped : list [tuple [str , str ]] = []
14801559 static_synced , static_skipped = (
@@ -1588,13 +1667,9 @@ def bind_path_c_in_region_parameter_views_into_bank(
15881667 "skipped" : [],
15891668 "in_region_parameter_count" : 0 ,
15901669 }
1591- prim_func = self .path_c_fused_train_block_prim_func (
1670+ abi_map , _abi_shapes , _prim_funcs = self .path_c_fused_train_block_physical_abi (
15921671 sequence_length = sequence_length ,
15931672 )
1594- abi_map = dict (
1595- getattr (prim_func , "_cppmega_path_c_physical_buffer_abi_map" , {})
1596- or {}
1597- )
15981673 bound : list [str ] = []
15991674 skipped : list [tuple [str , str ]] = []
16001675 static_synced , static_skipped = (
@@ -1714,6 +1789,190 @@ def path_c_fused_suffix_custom_function_available(self) -> bool:
17141789 getattr (self , "_path_c_fused_suffix_custom_function" , None )
17151790 )
17161791
1792+ def attach_path_c_fused_replay_custom_function (
1793+ self ,
1794+ fused_replay : Any ,
1795+ * ,
1796+ parameter_order : Sequence [str ],
1797+ first_in_region_layer_index : int ,
1798+ boundary_output_logical_names : Sequence [str ],
1799+ chunk_rows : int = DEFAULT_CHUNK_ROWS ,
1800+ ) -> None :
1801+ """Attach a fused-block replay custom function for loss replay.
1802+
1803+ The attached callable owns only the generated Path C block. The suffix
1804+ loss remains eager MLX code so ``lm_head`` and softmax loops never enter
1805+ the TileLang train-block PrimFunc.
1806+ """
1807+
1808+ if not callable (fused_replay ):
1809+ raise TypeError ("fused_replay must be callable" )
1810+ if first_in_region_layer_index < 0 :
1811+ raise ValueError ("first_in_region_layer_index must be non-negative" )
1812+ self ._path_c_fused_replay_custom_function = fused_replay
1813+ self ._path_c_fused_replay_parameter_order = tuple (parameter_order )
1814+ self ._path_c_fused_replay_first_in_region_layer_index = int (
1815+ first_in_region_layer_index
1816+ )
1817+ self ._path_c_fused_replay_boundary_output_logical_names = tuple (
1818+ str (name ) for name in boundary_output_logical_names
1819+ )
1820+ self ._path_c_fused_replay_chunk_rows = max (1 , int (chunk_rows ))
1821+
1822+ def detach_path_c_fused_replay_custom_function (self ) -> None :
1823+ for attr in (
1824+ "_path_c_fused_replay_custom_function" ,
1825+ "_path_c_fused_replay_parameter_order" ,
1826+ "_path_c_fused_replay_first_in_region_layer_index" ,
1827+ "_path_c_fused_replay_boundary_output_logical_names" ,
1828+ "_path_c_fused_replay_chunk_rows" ,
1829+ ):
1830+ if hasattr (self , attr ):
1831+ delattr (self , attr )
1832+
1833+ def path_c_fused_replay_custom_function_available (self ) -> bool :
1834+ return callable (
1835+ getattr (self , "_path_c_fused_replay_custom_function" , None )
1836+ )
1837+
1838+ def path_c_fused_replay_loss (
1839+ self ,
1840+ batch : Mapping [str , mx .array ],
1841+ ) -> tuple [mx .array , mx .array ]:
1842+ """Compute loss with eager suffix and fused-block replay VJP."""
1843+
1844+ fused_replay = getattr (
1845+ self , "_path_c_fused_replay_custom_function" , None
1846+ )
1847+ parameter_order : tuple [str , ...] = tuple (
1848+ cast ("Sequence[str]" ,
1849+ getattr (self , "_path_c_fused_replay_parameter_order" , ()))
1850+ )
1851+ first_in_region_layer_index = getattr (
1852+ self ,
1853+ "_path_c_fused_replay_first_in_region_layer_index" ,
1854+ None ,
1855+ )
1856+ boundary_output_names : tuple [str , ...] = tuple (
1857+ cast (
1858+ "Sequence[str]" ,
1859+ getattr (
1860+ self ,
1861+ "_path_c_fused_replay_boundary_output_logical_names" ,
1862+ (),
1863+ ),
1864+ )
1865+ )
1866+ if (
1867+ not callable (fused_replay )
1868+ or not parameter_order
1869+ or first_in_region_layer_index is None
1870+ or not boundary_output_names
1871+ ):
1872+ raise ValueError (
1873+ "path_c_fused_replay_loss requires "
1874+ "attach_path_c_fused_replay_custom_function to be installed"
1875+ )
1876+
1877+ from cppmega_mlx .data .batch import ensure_lm_batch
1878+
1879+ lm_batch = ensure_lm_batch (batch )
1880+ input_ids = lm_batch .inputs
1881+ targets = lm_batch .targets
1882+ target_mask = lm_batch .target_mask
1883+ side_channel_kwargs = dict (lm_batch .model_kwargs ())
1884+ document_ids = lm_batch .input_document_ids
1885+
1886+ if target_mask .shape != targets .shape :
1887+ raise ValueError (
1888+ "target_mask shape must match targets; got "
1889+ f"{ target_mask .shape } vs { targets .shape } "
1890+ )
1891+
1892+ hidden_entry = self .decoder_hidden_states (
1893+ input_ids ,
1894+ structure_ids = side_channel_kwargs .get ("structure_ids" ),
1895+ dep_levels = side_channel_kwargs .get ("dep_levels" ),
1896+ ast_depth_ids = side_channel_kwargs .get ("ast_depth_ids" ),
1897+ sibling_index_ids = side_channel_kwargs .get ("sibling_index_ids" ),
1898+ node_type_ids = side_channel_kwargs .get ("node_type_ids" ),
1899+ platform_ids = side_channel_kwargs .get ("platform_ids" ),
1900+ document_ids = document_ids ,
1901+ kv_cache = None ,
1902+ stop_layer_index = int (first_in_region_layer_index ),
1903+ apply_final_norm = False ,
1904+ )
1905+
1906+ params = tuple (
1907+ self ._path_c_get_parameter_tensor (name )
1908+ for name in parameter_order
1909+ )
1910+ if any (p is None for p in params ):
1911+ missing = [
1912+ name
1913+ for name , p in zip (parameter_order , params , strict = True )
1914+ if p is None
1915+ ]
1916+ raise ValueError (
1917+ "path_c_fused_replay_loss could not resolve in-region "
1918+ f"parameters: { missing } "
1919+ )
1920+ boundary_result : Any = fused_replay (
1921+ hidden_entry ,
1922+ * cast (tuple [mx .array , ...], params ),
1923+ )
1924+ boundary_outputs = (
1925+ boundary_result
1926+ if isinstance (boundary_result , tuple )
1927+ else (boundary_result ,)
1928+ )
1929+ if len (boundary_outputs ) != len (boundary_output_names ):
1930+ raise ValueError (
1931+ "fused replay returned "
1932+ f"{ len (boundary_outputs )} boundary outputs, expected "
1933+ f"{ len (boundary_output_names )} "
1934+ )
1935+ hidden = boundary_outputs [0 ]
1936+ for boundary in boundary_outputs [1 :]:
1937+ hidden = hidden + boundary
1938+
1939+ norm = getattr (self , "norm" , None )
1940+ lm_head = getattr (self , "lm_head" , None )
1941+ norm_weight = getattr (norm , "weight" , None )
1942+ head_weight = getattr (lm_head , "weight" , None )
1943+ if not isinstance (norm_weight , mx .array ):
1944+ raise TypeError (
1945+ "path_c_fused_replay_loss requires model.norm.weight as an mx.array"
1946+ )
1947+ if not isinstance (head_weight , mx .array ):
1948+ raise TypeError (
1949+ "path_c_fused_replay_loss requires model.lm_head.weight as an mx.array"
1950+ )
1951+ norm_eps = float (getattr (norm , "eps" , 1e-5 ))
1952+ inv_rms = mx .rsqrt (
1953+ mx .mean (hidden * hidden , axis = - 1 , keepdims = True )
1954+ + mx .array (norm_eps , dtype = hidden .dtype )
1955+ )
1956+ normed = hidden * inv_rms * norm_weight
1957+ token_losses = linear_cross_entropy (
1958+ normed ,
1959+ head_weight ,
1960+ targets ,
1961+ reduction = "none" ,
1962+ chunk_rows = int (
1963+ getattr (
1964+ self ,
1965+ "_path_c_fused_replay_chunk_rows" ,
1966+ DEFAULT_CHUNK_ROWS ,
1967+ )
1968+ ),
1969+ eval_chunks = False ,
1970+ )
1971+ ntokens = target_mask .sum ()
1972+ denom = mx .maximum (ntokens , mx .array (1.0 , dtype = mx .float32 ))
1973+ loss = (token_losses * target_mask ).astype (mx .float32 ).sum () / denom
1974+ return loss , ntokens .astype (mx .uint32 )
1975+
17171976 def path_c_fused_suffix_loss (
17181977 self ,
17191978 batch : Mapping [str , mx .array ],
0 commit comments