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