Skip to content

Commit 3d244ae

Browse files
committed
fix: resolve weight sync and checkpointing issues in RL training
1 parent 7b0e798 commit 3d244ae

4 files changed

Lines changed: 71 additions & 50 deletions

File tree

src/maxtext/integration/vllm/maxtext_vllm_rollout.py

Lines changed: 48 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
5463
def 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

130152
class 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

src/maxtext/trainers/post_train/rl/train_rl.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ def build_reward_fns(trainer_config: Any, make_reward_fn: Callable) -> list:
381381
]
382382

383383

384-
def create_rl_components(
384+
def create_rl_components( # pylint: disable=too-many-positional-arguments
385385
trainer_config,
386386
sampler_config,
387387
sampler_devices,

tests/end_to_end/tpu/gemma3/4b/test_gemma3_rl.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ python3 -m maxtext.trainers.post_train.rl.train_rl \
4343
model_name=${MODEL_NAME} enable_single_controller=${use_pathways} \
4444
checkpoint_storage_use_zarr3=False checkpoint_storage_use_ocdbt=False \
4545
rollout_tensor_parallelism=1 \
46+
use_standalone_converter=true \
4647
vllm_hf_overrides='{architectures: ["MaxTextForCausalLM"]}' \
4748
vllm_additional_config='{"maxtext_config": {"model_name": "gemma3-4b", "log_config": "false"}}'
4849

tests/post_training/unit/vllm_rollout_unroll_test.py

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -99,22 +99,24 @@ def test_correctly_unrolls_gemma_scanned_weights(self):
9999
decoder_dict = unrolled["decoder"]
100100

101101
# Should contain keys 0 to 6 under layers
102-
self.assertIn("0", decoder_dict["layers"])
103-
self.assertIn("1", decoder_dict["layers"])
104-
self.assertIn("2", decoder_dict["layers"])
105-
self.assertIn("3", decoder_dict["layers"])
106-
self.assertIn("4", decoder_dict["layers"])
107-
self.assertIn("5", decoder_dict["layers"])
108-
self.assertIn("6", decoder_dict["layers"])
102+
self.assertIn(0, decoder_dict["layers"])
103+
self.assertIn(1, decoder_dict["layers"])
104+
self.assertIn(2, decoder_dict["layers"])
105+
self.assertIn(3, decoder_dict["layers"])
106+
self.assertIn(4, decoder_dict["layers"])
107+
self.assertIn(5, decoder_dict["layers"])
108+
self.assertIn(6, decoder_dict["layers"])
109+
110+
self.assertIsInstance(list(decoder_dict["layers"].keys())[0], int)
109111

110112
# Check that values are correctly sliced
111-
np.testing.assert_array_equal(decoder_dict["layers"]["0"]["attn"]["wq"], np.array([[0], [0]]))
112-
np.testing.assert_array_equal(decoder_dict["layers"]["1"]["attn"]["wq"], np.array([[1], [1]]))
113-
np.testing.assert_array_equal(decoder_dict["layers"]["2"]["attn"]["wq"], np.array([[2], [2]]))
114-
np.testing.assert_array_equal(decoder_dict["layers"]["3"]["attn"]["wq"], np.array([[3], [3]]))
115-
np.testing.assert_array_equal(decoder_dict["layers"]["4"]["attn"]["wq"], np.array([[4], [4]]))
116-
np.testing.assert_array_equal(decoder_dict["layers"]["5"]["attn"]["wq"], np.array([[5], [5]]))
117-
np.testing.assert_array_equal(decoder_dict["layers"]["6"]["attn"]["wq"], np.array([[6], [6]]))
113+
np.testing.assert_array_equal(decoder_dict["layers"][0]["attn"]["wq"], np.array([[0], [0]]))
114+
np.testing.assert_array_equal(decoder_dict["layers"][1]["attn"]["wq"], np.array([[1], [1]]))
115+
np.testing.assert_array_equal(decoder_dict["layers"][2]["attn"]["wq"], np.array([[2], [2]]))
116+
np.testing.assert_array_equal(decoder_dict["layers"][3]["attn"]["wq"], np.array([[3], [3]]))
117+
np.testing.assert_array_equal(decoder_dict["layers"][4]["attn"]["wq"], np.array([[4], [4]]))
118+
np.testing.assert_array_equal(decoder_dict["layers"][5]["attn"]["wq"], np.array([[5], [5]]))
119+
np.testing.assert_array_equal(decoder_dict["layers"][6]["attn"]["wq"], np.array([[6], [6]]))
118120

119121
@pytest.mark.cpu_only
120122
def test_correctly_unrolls_gemma3_gemma4_scanned_blocks(self):
@@ -150,7 +152,8 @@ def test_correctly_unrolls_gemma3_gemma4_scanned_blocks(self):
150152
unrolled = unroll_gemma_scanned_weights(weights)
151153

152154
decoder_dict = unrolled["decoder"]
153-
self.assertIn("0", decoder_dict["layers"])
154-
self.assertIn("6", decoder_dict["layers"])
155-
np.testing.assert_array_equal(decoder_dict["layers"]["0"]["attn"]["wq"], np.array([[0], [0]]))
156-
np.testing.assert_array_equal(decoder_dict["layers"]["6"]["attn"]["wq"], np.array([[6], [6]]))
155+
self.assertIn(0, decoder_dict["layers"])
156+
self.assertIn(6, decoder_dict["layers"])
157+
self.assertIsInstance(list(decoder_dict["layers"].keys())[0], int)
158+
np.testing.assert_array_equal(decoder_dict["layers"][0]["attn"]["wq"], np.array([[0], [0]]))
159+
np.testing.assert_array_equal(decoder_dict["layers"][6]["attn"]["wq"], np.array([[6], [6]]))

0 commit comments

Comments
 (0)