@@ -51,21 +51,38 @@ def _create_model_converter(model_name: str, config: Any, mesh: jax.sharding.Mes
5151 return None
5252
5353
54+ def _find_scanned_layer_idx (key_tuple , container_names = ("layers" , "scanned_blocks" , "layers_remainder" )):
55+ """Returns (container_idx, container_name) if a scanned layer structure is found, else (-1, None)."""
56+ for name in container_names :
57+ for i in range (len (key_tuple ) - 1 ):
58+ if key_tuple [i ] == name and isinstance (key_tuple [i + 1 ], str ) and key_tuple [i + 1 ].startswith ("layers_" ):
59+ return i , name
60+ return - 1 , None
61+
62+
5463def unroll_gemma_scanned_weights (weights ):
5564 """Workaround for tunix unstacking bug with Gemma 3/4 scanned blocks.
5665
57- tunix fails to map nested layers like `layers.layers_0` to `layers_X`.
58- We manually unroll them here if we detect the structure.
66+ tunix fails to map nested layers like `layers.layers_0` to `layers_X`
67+ if the target expects integer keys (as in nnx.List).
68+ We manually unroll them here, keeping the keys as tuples with integers.
5969 """
60- if not hasattr (weights , "to_pure_dict" ):
70+ if hasattr (weights , "to_pure_dict" ):
71+ pure_dict = weights .to_pure_dict ()
72+ elif hasattr (weights , "to_dict" ):
73+ pure_dict = weights .to_dict ()
74+ elif isinstance (weights , dict ):
75+ pure_dict = weights
76+ else :
6177 return weights
6278
63- flat_w = flatten_dict (weights . to_pure_dict (), sep = "/" )
79+ flat_w = flatten_dict (pure_dict )
6480 new_flat_w = {}
6581
82+ logging .debug ("MaxTextVllmSampler: First 5 keys in flat_w: %s" , list (flat_w .keys ())[:5 ])
83+
6684 # Check if this is actually a scanned Gemma 3/4 checkpoint
67- # by looking for the scanned nested structure.
68- is_gemma_scanned = any ("decoder/layers/layers_0/" in k or "decoder/scanned_blocks/layers_0/" in k for k in flat_w )
85+ is_gemma_scanned = any (_find_scanned_layer_idx (k )[0 ] != - 1 for k in flat_w )
6986
7087 if not is_gemma_scanned :
7188 return weights
@@ -76,27 +93,28 @@ def unroll_gemma_scanned_weights(weights):
7693 pattern_keys = set ()
7794 scan_length = 0
7895 for k , v in flat_w .items ():
79- if "decoder/layers/layers_" in k or "decoder/scanned_blocks/layers_" in k :
80- layer_sub_idx = k . split ( "layers_" )[ - 1 ]. split ( "/" )[ 0 ]
81- pattern_keys . add ( int (layer_sub_idx ) )
82- # In MaxText, Gemma uses param_scan_axis=1, so the scan dimension is at axis 1
83- if hasattr (v , "shape" ) and len (v .shape ) > 1 :
96+ container_idx , name = _find_scanned_layer_idx ( k )
97+ if container_idx != - 1 and name != "layers_remainder" :
98+ layer_sub_idx = int (k [ container_idx + 1 ]. split ( "layers_" )[ 1 ] )
99+ pattern_keys . add ( layer_sub_idx )
100+ if hasattr (v , "shape" ) and len (v .shape ) >= 2 :
84101 scan_length = max (scan_length , v .shape [1 ])
85102
86103 pattern_length = max (pattern_keys ) + 1 if pattern_keys else 0
87104 logging .info ("MaxTextVllmSampler: Discovered scan_length=%d, pattern_length=%d" , scan_length , pattern_length )
88105
89106 unrolled_count = 0
90107 for k , v in flat_w .items ():
91- if "decoder/layers/layers_" in k or "decoder/scanned_blocks/layers_" in k :
92- # Unstack the array along the 1st axis
93- if "decoder/scanned_blocks/layers_" in k :
94- parts = k .split ("decoder/scanned_blocks/layers_" )
95- else :
96- parts = k .split ("decoder/layers/layers_" )
108+ if "dropout" in k or "rngs" in k :
109+ new_flat_w [k ] = v
110+ continue
111+
112+ container_idx , container_name = _find_scanned_layer_idx (k )
97113
98- layer_sub_idx = int (parts [1 ].split ("/" )[0 ])
99- suffix = "/" + "/" .join (parts [1 ].split ("/" )[1 :])
114+ if container_idx != - 1 and container_name in ("layers" , "scanned_blocks" ):
115+ layer_sub_idx = int (k [container_idx + 1 ].split ("layers_" )[1 ])
116+ prefix = k [:container_idx ] + ("layers" ,)
117+ suffix = k [container_idx + 2 :]
100118
101119 if hasattr (v , "shape" ) and len (v .shape ) > 1 :
102120 v_swapped = jnp .swapaxes (v , 1 , 0 )
@@ -106,25 +124,29 @@ def unroll_gemma_scanned_weights(weights):
106124
107125 for i in range (scan_length ):
108126 global_idx = i * pattern_length + layer_sub_idx
109- # Map back to nnx.List format which uses layers/X/ instead of layers_X
110- new_flat_w [f"decoder/layers/ { global_idx } { suffix } " ] = unstacked [i ]
127+ new_k = prefix + ( global_idx ,) + suffix
128+ new_flat_w [new_k ] = unstacked [i ]
111129 unrolled_count += 1
112130
113- elif "decoder/layers_remainder/layers_" in k :
114- layer_sub_idx = int (k .split ("decoder/layers_remainder/layers_" )[1 ].split ("/" )[0 ])
115- suffix = "/" + "/" .join (k .split ("decoder/layers_remainder/layers_" )[1 ].split ("/" )[1 :])
131+ elif container_idx != - 1 and container_name == "layers_remainder" :
132+ layer_sub_idx = int (k [container_idx + 1 ].split ("layers_" )[1 ])
133+ prefix = k [:container_idx ] + ("layers" ,)
134+ suffix = k [container_idx + 2 :]
116135
117136 global_idx = scan_length * pattern_length + layer_sub_idx
118- new_flat_w [f"decoder/layers/{ global_idx } { suffix } " ] = v
137+ new_k = prefix + (global_idx ,) + suffix
138+ new_flat_w [new_k ] = v
119139 unrolled_count += 1
120140 else :
121141 new_flat_w [k ] = v
122142
143+ assert unrolled_count > 0 , "MaxTextVllmSampler: Detected scanned structure, but failed to unroll any layers!"
144+
123145 logging .info (
124146 "MaxTextVllmSampler: Successfully unrolled %d scanned tensor components into vLLM-compatible nnx.List format." ,
125147 unrolled_count ,
126148 )
127- return unflatten_dict (new_flat_w , sep = "/" )
149+ return unflatten_dict (new_flat_w )
128150
129151
130152class MaxTextVllmSampler (VllmSampler ):
@@ -152,12 +174,7 @@ def update_params(
152174 ):
153175 """Update the vLLM runner weights from a MaxText state tree."""
154176 if self ._converter is None :
155- # --- Workaround for tunix unstacking bug with Gemma 3/4 scanned blocks ---
156- # tunix fails to map nested layers like `layers.layers_0` to `layers_X`.
157- # We manually unroll them here if we detect the structure.
158177 updated_weights = unroll_gemma_scanned_weights (updated_weights )
159- # --- End Workaround ---
160-
161178 super ().update_params (updated_weights , filter_types )
162179 return None
163180
0 commit comments