Skip to content

Commit 70c1883

Browse files
committed
feat(nnx): Add Flax NNX PEFT/LoRA support and checkpoint resume
- Resolve pjit sharding metadata mismatches by re-evaluating state_mesh_shardings after parameter restoration and LoRA/QLoRA module injection. - Update l2norm_pytree to skip non-inexact leaves (e.g. packed void arrays or WithAux wrappers). - Implement normalized key translation (0 <-> layers_0) in checkpointing and model creation utilities for seamless parameter merging across NNX and Linen models. - Add mesh axis shape sanitization for non-divisible dimensions. - Include unit tests for l2norm_pytree void quantization, key translation, and upfront LoRA state restoration.
1 parent ad841c4 commit 70c1883

14 files changed

Lines changed: 1499 additions & 161 deletions

File tree

run_lora_checkpoint_resume_test.sh

Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
#!/usr/bin/env bash
2+
# Copyright 2026 Google LLC
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# https://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
set -o pipefail
17+
18+
# Detect directories
19+
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
20+
WORKSPACE_DIR="${SCRIPT_DIR}"
21+
22+
VENV_PYTHON="/home/jackyf_google_com/maxtext/.venv/bin/python"
23+
if [ ! -f "${VENV_PYTHON}" ]; then
24+
VENV_PYTHON="${WORKSPACE_DIR}/.venv/bin/python"
25+
fi
26+
if [ ! -f "${VENV_PYTHON}" ]; then
27+
VENV_PYTHON="python3"
28+
fi
29+
30+
export PYTHONPATH="${WORKSPACE_DIR}/src"
31+
32+
# Parse CLI arguments and options
33+
SPECIFIED_MODEL=""
34+
SPECIFIED_TRAINER=""
35+
SPECIFIED_STEP="all"
36+
SCAN_LAYERS_VAL="True"
37+
LORA_QTYPE="nf4"
38+
39+
while [[ $# -gt 0 ]]; do
40+
case "$1" in
41+
-m|--model)
42+
SPECIFIED_MODEL="$2"
43+
shift 2
44+
;;
45+
-t|--trainer)
46+
SPECIFIED_TRAINER="$2"
47+
shift 2
48+
;;
49+
-s|--step)
50+
SPECIFIED_STEP="$2"
51+
shift 2
52+
;;
53+
--scan|--scan-layers)
54+
SCAN_LAYERS_VAL="$2"
55+
shift 2
56+
;;
57+
-q|--qtype)
58+
LORA_QTYPE="$2"
59+
shift 2
60+
;;
61+
-h|--help)
62+
echo "Usage: $0 [-m|--model MODEL_NAME] [-t|--trainer TRAINER_TYPE] [-s|--step STEP_NUM] [--scan True|False] [-q|--qtype nf4|int8]"
63+
exit 0
64+
;;
65+
*)
66+
if [ -z "${SPECIFIED_MODEL}" ]; then
67+
SPECIFIED_MODEL="$1"
68+
elif [ -z "${SPECIFIED_TRAINER}" ]; then
69+
SPECIFIED_TRAINER="$1"
70+
fi
71+
shift
72+
;;
73+
esac
74+
done
75+
76+
# Fallback to environment variables if set
77+
SPECIFIED_MODEL="${SPECIFIED_MODEL:-${TEST_MODEL_NAME}}"
78+
SPECIFIED_TRAINER="${SPECIFIED_TRAINER:-${TEST_TRAINER}}"
79+
80+
if [ -n "${SPECIFIED_MODEL}" ]; then
81+
MODELS=("${SPECIFIED_MODEL}")
82+
else
83+
MODELS=("gemma4-26b" "qwen3-4b" "llama3.1-8b" "gpt-oss-20b")
84+
fi
85+
86+
if [ -n "${SPECIFIED_TRAINER}" ]; then
87+
TRAINERS=("${SPECIFIED_TRAINER}")
88+
else
89+
TRAINERS=("pre_train" "sft_native" "sft_custom")
90+
fi
91+
92+
echo "=========================================================="
93+
echo "Starting Flax NNX LoRA Comprehensive E2E Test Suite"
94+
echo "Models under test: ${MODELS[*]}"
95+
echo "Trainers under test:${TRAINERS[*]}"
96+
echo "Scan Layers: ${SCAN_LAYERS_VAL}"
97+
echo "Workspace: ${WORKSPACE_DIR}"
98+
echo "Python: ${VENV_PYTHON}"
99+
echo "PYTHONPATH: ${PYTHONPATH}"
100+
echo "=========================================================="
101+
102+
LOG_DIR="${WORKSPACE_DIR}/maxtext_output/lora_resume_test_logs"
103+
mkdir -p "${LOG_DIR}"
104+
105+
for TEST_MODEL_NAME in "${MODELS[@]}"; do
106+
for TRAINER in "${TRAINERS[@]}"; do
107+
echo -e "\n=========================================================="
108+
echo "TESTING MODEL: ${TEST_MODEL_NAME} | TRAINER: ${TRAINER}"
109+
echo "=========================================================="
110+
111+
# Setup module and config paths
112+
if [ "${TRAINER}" == "pre_train" ]; then
113+
MODULE_NAME="maxtext.trainers.pre_train.train"
114+
CONFIG_PATH="src/maxtext/configs/base.yml"
115+
elif [ "${TRAINER}" == "sft_native" ]; then
116+
MODULE_NAME="maxtext.trainers.post_train.sft.train_sft_native"
117+
CONFIG_PATH="src/maxtext/configs/base.yml"
118+
elif [ "${TRAINER}" == "sft_custom" ]; then
119+
MODULE_NAME="maxtext.trainers.post_train.sft.train_sft"
120+
CONFIG_PATH="src/maxtext/configs/post_train/sft.yml"
121+
fi
122+
123+
# Directories
124+
BASE_RUN="lora_resume_test_${TEST_MODEL_NAME}_${TRAINER}_base"
125+
WORKLOAD_RUN="lora_resume_test_${TEST_MODEL_NAME}_${TRAINER}_workload"
126+
127+
if [ "${SPECIFIED_STEP}" = "all" ] || [ "${SPECIFIED_STEP}" = "1" ]; then
128+
rm -rf "${WORKSPACE_DIR}/maxtext_output/${BASE_RUN}"
129+
rm -rf "${WORKSPACE_DIR}/maxtext_output/${WORKLOAD_RUN}"
130+
elif [ "${SPECIFIED_STEP}" = "2" ]; then
131+
rm -rf "${WORKSPACE_DIR}/maxtext_output/${WORKLOAD_RUN}"
132+
fi
133+
134+
LOG_PREFIX="${LOG_DIR}/${TEST_MODEL_NAME}_${TRAINER}"
135+
136+
# 1. Generate base-only checkpoint
137+
if [ "${SPECIFIED_STEP}" = "all" ] || [ "${SPECIFIED_STEP}" = "1" ]; then
138+
echo "[1/4] Generating base-only checkpoint..."
139+
"${VENV_PYTHON}" -m "${MODULE_NAME}" "${CONFIG_PATH}" \
140+
run_name="${BASE_RUN}" \
141+
model_name="${TEST_MODEL_NAME}" scan_layers=${SCAN_LAYERS_VAL} pure_nnx=True dataset_type=synthetic steps=10 \
142+
enable_checkpointing=True checkpoint_period=10 \
143+
enable_goodput_recording=False enable_checkpoint_cloud_logger=False monitor_goodput=False log_period=1 \
144+
override_model_config=True base_num_decoder_layers=2 base_emb_dim=128 base_mlp_dim=256 base_num_query_heads=4 base_num_kv_heads=4 head_dim=128 \
145+
max_target_length=128 vocab_size=256 per_device_batch_size=8 \
146+
lora.enable_lora=False \
147+
> "${LOG_PREFIX}_step1_base.log" 2>&1
148+
STEP1_STATUS=$?
149+
echo "Base Checkpoint Exit Status: ${STEP1_STATUS}"
150+
if [ ${STEP1_STATUS} -ne 0 ]; then
151+
echo "Error: ${TEST_MODEL_NAME} ${TRAINER} base checkpoint generation failed! See logs in ${LOG_PREFIX}_step1_base.log"
152+
exit 1
153+
fi
154+
fi
155+
156+
# Find items or model_params based on trainer layout
157+
BASE_CHECKPOINT_PATH=$(find "${WORKSPACE_DIR}/maxtext_output/${BASE_RUN}/checkpoints" -name "items" -o -name "model_params" | head -n 1)
158+
if [ -z "${BASE_CHECKPOINT_PATH}" ] || [ ! -d "${BASE_CHECKPOINT_PATH}" ]; then
159+
echo "Error: Could not find generated base checkpoint directory under maxtext_output/${BASE_RUN}/checkpoints"
160+
exit 1
161+
fi
162+
echo "Found Base Checkpoint: ${BASE_CHECKPOINT_PATH}"
163+
164+
# 2. Train with LoRA (saves checkpoint at step 10)
165+
if [ "${SPECIFIED_STEP}" = "all" ] || [ "${SPECIFIED_STEP}" = "2" ]; then
166+
sleep 2
167+
echo "[2/4] Training with LoRA starting from base checkpoint..."
168+
"${VENV_PYTHON}" -m "${MODULE_NAME}" "${CONFIG_PATH}" \
169+
run_name="${WORKLOAD_RUN}" \
170+
model_name="${TEST_MODEL_NAME}" scan_layers=${SCAN_LAYERS_VAL} pure_nnx=True dataset_type=synthetic steps=15 \
171+
load_parameters_path="${BASE_CHECKPOINT_PATH}" \
172+
enable_checkpointing=True checkpoint_period=10 \
173+
enable_goodput_recording=False enable_checkpoint_cloud_logger=False monitor_goodput=False log_period=1 \
174+
override_model_config=True base_num_decoder_layers=2 base_emb_dim=128 base_mlp_dim=256 base_num_query_heads=4 base_num_kv_heads=4 head_dim=128 \
175+
max_target_length=128 vocab_size=256 per_device_batch_size=8 \
176+
lora.enable_lora=True lora.lora_rank=4 lora.lora_alpha=8.0 lora.lora_weight_qtype=${LORA_QTYPE} lora.lora_tile_size=16 sharding_tolerance=1.0 \
177+
> "${LOG_PREFIX}_step2_lora_train.log" 2>&1
178+
STEP2_STATUS=$?
179+
echo "LoRA Train Exit Status: ${STEP2_STATUS}"
180+
if [ ${STEP2_STATUS} -ne 0 ]; then
181+
echo "Error: ${TEST_MODEL_NAME} ${TRAINER} LoRA initial training failed! See logs in ${LOG_PREFIX}_step2_lora_train.log"
182+
exit 1
183+
fi
184+
fi
185+
186+
# Find lora checkpoint folder (usually 10/items or 10/model_params)
187+
LORA_CHECKPOINT_PATH=$(find "${WORKSPACE_DIR}/maxtext_output/${WORKLOAD_RUN}/checkpoints/10" -name "items" -o -name "model_params" | head -n 1)
188+
if [ -z "${LORA_CHECKPOINT_PATH}" ] || [ ! -d "${LORA_CHECKPOINT_PATH}" ]; then
189+
LORA_CHECKPOINT_PATH=$(find "${WORKSPACE_DIR}/maxtext_output/${WORKLOAD_RUN}/checkpoints" -name "items" -o -name "model_params" | grep -v "test_base" | head -n 1)
190+
fi
191+
echo "Found Saved LoRA Checkpoint: ${LORA_CHECKPOINT_PATH}"
192+
193+
# 3. Resume training from step 10 under same run_name
194+
if [ "${SPECIFIED_STEP}" = "all" ] || [ "${SPECIFIED_STEP}" = "3" ]; then
195+
sleep 2
196+
echo "[3/4] Resuming training under same run name (workload name)..."
197+
"${VENV_PYTHON}" -m "${MODULE_NAME}" "${CONFIG_PATH}" \
198+
run_name="${WORKLOAD_RUN}" \
199+
model_name="${TEST_MODEL_NAME}" scan_layers=${SCAN_LAYERS_VAL} pure_nnx=True dataset_type=synthetic steps=20 \
200+
enable_checkpointing=True checkpoint_period=10 \
201+
enable_goodput_recording=False enable_checkpoint_cloud_logger=False monitor_goodput=False log_period=1 \
202+
override_model_config=True base_num_decoder_layers=2 base_emb_dim=128 base_mlp_dim=256 base_num_query_heads=4 base_num_kv_heads=4 head_dim=128 \
203+
max_target_length=128 vocab_size=256 per_device_batch_size=8 \
204+
lora.enable_lora=True lora.lora_rank=4 lora.lora_alpha=8.0 lora.lora_weight_qtype=${LORA_QTYPE} lora.lora_tile_size=16 sharding_tolerance=1.0 \
205+
> "${LOG_PREFIX}_step3_lora_resume.log" 2>&1
206+
STEP3_STATUS=$?
207+
echo "LoRA Resume Exit Status: ${STEP3_STATUS}"
208+
if [ ${STEP3_STATUS} -ne 0 ]; then
209+
echo "Error: ${TEST_MODEL_NAME} ${TRAINER} LoRA resume training failed! See logs in ${LOG_PREFIX}_step3_lora_resume.log"
210+
exit 1
211+
fi
212+
fi
213+
214+
# 4. Verify standalone restore of saved LoRA checkpoint
215+
if [ "${SPECIFIED_STEP}" = "all" ] || [ "${SPECIFIED_STEP}" = "4" ]; then
216+
sleep 2
217+
echo "[4/4] Verifying standalone restore of LoRA checkpoint..."
218+
"${VENV_PYTHON}" -m "${MODULE_NAME}" "${CONFIG_PATH}" \
219+
run_name="lora_restore_verify_${TEST_MODEL_NAME}_${TRAINER}_$(date +%s)" \
220+
model_name="${TEST_MODEL_NAME}" scan_layers=${SCAN_LAYERS_VAL} pure_nnx=True dataset_type=synthetic steps=11 \
221+
load_parameters_path="${BASE_CHECKPOINT_PATH}" \
222+
lora.lora_restore_path="${LORA_CHECKPOINT_PATH}" \
223+
enable_checkpointing=True checkpoint_period=1000 \
224+
enable_goodput_recording=False enable_checkpoint_cloud_logger=False monitor_goodput=False log_period=1 \
225+
override_model_config=True base_num_decoder_layers=2 base_emb_dim=128 base_mlp_dim=256 base_num_query_heads=4 base_num_kv_heads=4 head_dim=128 \
226+
max_target_length=128 vocab_size=256 per_device_batch_size=8 \
227+
lora.enable_lora=True lora.lora_rank=4 lora.lora_alpha=8.0 lora.lora_weight_qtype=${LORA_QTYPE} lora.lora_tile_size=16 sharding_tolerance=1.0 \
228+
> "${LOG_PREFIX}_step4_lora_restore.log" 2>&1
229+
STEP4_STATUS=$?
230+
echo "LoRA Standalone Restore Exit Status: ${STEP4_STATUS}"
231+
if [ ${STEP4_STATUS} -ne 0 ]; then
232+
echo "Error: ${TEST_MODEL_NAME} ${TRAINER} LoRA standalone restore failed! See logs in ${LOG_PREFIX}_step4_lora_restore.log"
233+
exit 1
234+
fi
235+
fi
236+
237+
done
238+
done
239+
240+
echo ""
241+
echo "=========================================================="
242+
echo "Asserting Accuracy and Parsing Performance Metrics"
243+
echo "=========================================================="
244+
245+
"${VENV_PYTHON}" -c "
246+
import glob
247+
import re
248+
import os
249+
250+
log_dir = '${LOG_DIR}'
251+
models = [$(printf '"%s", ' "${MODELS[@]}")]
252+
trainers = [$(printf '"%s", ' "${TRAINERS[@]}")]
253+
254+
# Find all log files
255+
log_files = glob.glob(os.path.join(log_dir, '*.log'))
256+
257+
results = []
258+
all_passed = True
259+
260+
def parse_loss(filepath, target_step):
261+
if not os.path.exists(filepath):
262+
return None, f'File not found: {os.path.basename(filepath)}'
263+
with open(filepath, 'r') as f:
264+
content = f.read()
265+
pattern = rf'completed step: {target_step},.*loss: ([\d\.]+)'
266+
match = re.search(pattern, content)
267+
if match:
268+
return float(match.group(1)), None
269+
return None, f'Step {target_step} not found in {os.path.basename(filepath)}'
270+
271+
print('\n### E2E LoRA Core Verification Results\n')
272+
print('| Model | Trainer | Final Train Loss | Initial Resume Loss | Loss Continuity | Standalone Restore | Status |')
273+
print('|---|---|---|---|---|---|---|')
274+
275+
for model in models:
276+
for trainer in trainers:
277+
step2_log = os.path.join(log_dir, f'{model}_{trainer}_step2_lora_train.log')
278+
step3_log = os.path.join(log_dir, f'{model}_{trainer}_step3_lora_resume.log')
279+
step4_log = os.path.join(log_dir, f'{model}_{trainer}_step4_lora_restore.log')
280+
281+
# SFT trainers save/resume at step 10/11 vs pre_train step 10/11
282+
train_step = 15 if trainer == 'sft_custom' else 14
283+
resume_step = 16 if trainer == 'sft_custom' else 15
284+
285+
train_loss, err2 = parse_loss(step2_log, train_step)
286+
resume_loss, err3 = parse_loss(step3_log, resume_step)
287+
288+
restore_passed = os.path.exists(step4_log)
289+
if restore_passed:
290+
with open(step4_log, 'r') as f:
291+
restore_content = f.read()
292+
restore_passed = 'completed step: 10' in restore_content or 'completed step: 0' in restore_content
293+
294+
if train_loss is not None and resume_loss is not None:
295+
diff = abs(train_loss - resume_loss)
296+
rel_diff = diff / max(abs(train_loss), 1e-5)
297+
loss_passed = diff < 0.05 or rel_diff < 0.05
298+
continuity_str = f'PASSED' if loss_passed else f'FAILED (Diff: {diff:.6f}, RelDiff: {rel_diff:.4f})'
299+
else:
300+
loss_passed = False
301+
continuity_str = 'FAILED (Logs empty)'
302+
303+
restore_str = 'PASSED' if restore_passed else 'FAILED'
304+
row_passed = loss_passed and restore_passed
305+
if not row_passed:
306+
all_passed = False
307+
308+
status_str = 'PASSED' if row_passed else 'FAILED'
309+
train_str = f'{train_loss:.6f} (Step {train_step})' if train_loss else 'N/A'
310+
resume_str = f'{resume_loss:.6f} (Step {resume_step})' if resume_loss else 'N/A'
311+
312+
print(f'| {model} | {trainer} | {train_str} | {resume_str} | {continuity_str} | {restore_str} | {status_str} |')
313+
314+
if not all_passed:
315+
print('\nFAILURE: One or more correctness assertions failed across the models/trainers.')
316+
exit(1)
317+
318+
print('\nSUCCESS: All Flax NNX LoRA E2E correctness assertions passed successfully!')
319+
"
320+
321+
echo "COMPREHENSIVE TEST COMPLETE."

0 commit comments

Comments
 (0)