Skip to content

Commit 84b50f9

Browse files
Merge pull request AI-Hypercomputer#4250 from AI-Hypercomputer:sft_native_refactor
PiperOrigin-RevId: 937468091
2 parents c7b9871 + c137f73 commit 84b50f9

2 files changed

Lines changed: 86 additions & 215 deletions

File tree

docs/tutorials/posttraining/multimodal.md

Lines changed: 79 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -27,46 +27,85 @@ Multimodal Large Language Models (LLMs) extend traditional text-only models by i
2727
![Illustration of multimodal MaxText.](../../_static/multimodal_overview.png)
2828
*Figure 1: Overview of multimodal dataflow in MaxText.*
2929

30-
## Checkpoint Conversion
30+
## Install MaxText
31+
32+
For instructions on installing MaxText on your VM, please refer to the [official documentation](../../install_maxtext.md#from-source) and use the `maxtext[tpu]` installation path to include all necessary dependencies.
3133

32-
Recently we have onboarded a new centralized tool for bidirectional checkpoint conversion between MaxText and HuggingFace ([README](https://github.com/AI-Hypercomputer/maxtext/blob/main/src/maxtext/checkpoint_conversion/README.md)).
34+
> **Note:** If you have previously installed MaxText with a different option, we strongly recommend using a fresh virtual environment for `maxtext[tpu]` to avoid potential library version conflicts.
3335
34-
Install pytorch:
36+
## Setup environment variables
3537

38+
Login to Hugging Face. Provide your access token when prompted:
39+
40+
```bash
41+
hf auth login
3642
```
37-
python3 -m pip install torch --index-url https://download.pytorch.org/whl/cpu
43+
44+
Set up the following environment variables to configure your training run. Replace
45+
placeholders with your actual values.
46+
47+
```bash
48+
# -- Model configuration --
49+
# The MaxText model name. See `src/maxtext/configs/types.py` for `ModelName` for a
50+
# full list of supported models.
51+
export MODEL=<MODEL_NAME> # e.g., 'gemma3-4b'
52+
53+
# -- MaxText configuration --
54+
# Use a GCS bucket you own to store logs and checkpoints. Ideally in the same
55+
# region as your TPUs to minimize latency and costs.
56+
# You can list your buckets and their locations in the
57+
# [Cloud Console](https://console.cloud.google.com/storage/browser).
58+
export BASE_OUTPUT_DIRECTORY=<GCS_BUCKET> # e.g., gs://my-bucket/maxtext-runs
59+
60+
# An arbitrary string to identify this specific run.
61+
# We recommend to include the model, user, and timestamp.
62+
export RUN_NAME=<RUN_NAME>
63+
64+
export STEPS=<STEPS> # e.g., 1000
3865
```
3966

40-
Then use this command to convert an unscanned checkpoint from HuggingFace to MaxText, and save it to `MAXTEXT_CKPT_PATH`:
67+
## Checkpoint Conversion
68+
69+
This section explains how to prepare your model checkpoint for use with MaxText.
70+
71+
### Option 1: Using an existing MaxText checkpoint
72+
73+
If you already have a MaxText-compatible model checkpoint, simply set the following environment variable and move on to the next section.
74+
75+
```sh
76+
export MAXTEXT_CKPT_PATH=<CKPT_PATH> # e.g., gs://my-bucket/my-model-checkpoint/0/items
77+
```
78+
79+
### Option 2: Converting a Hugging Face checkpoint
80+
81+
Refer to [Hugging Face to MaxText](hf-to-maxtext) to convert a hugging face checkpoint to MaxText. Make sure you have correct checkpoint files converted and saved. Use this command to convert an unscanned checkpoint from HuggingFace to MaxText:
82+
83+
```sh
84+
python3 -m pip install torch --index-url https://download.pytorch.org/whl/cpu
4185

42-
```shell
43-
# Your Hugging Face access token. Required to download gated models like Llama.
44-
# You can generate one at https://huggingface.co/settings/tokens.
4586
# We explicitly set lazy_load_tensors to False here as lazy loading of tensors
4687
# is not supported when use_multimodal is True.
47-
export HF_TOKEN=<Hugging Face access token>
48-
export MAXTEXT_CKPT_PATH=<Checkpoint GCS path> # gs://my-bucket/path
49-
python -m maxtext.checkpoint_conversion.to_maxtext \
50-
model_name=gemma3-4b \
51-
hf_access_token=${HF_TOKEN?} \
52-
base_output_directory=${MAXTEXT_CKPT_PATH?} \
88+
python3 -m maxtext.checkpoint_conversion.to_maxtext \
89+
model_name=${MODEL?} \
90+
base_output_directory=${BASE_OUTPUT_DIRECTORY?} \
5391
use_multimodal=true \
5492
scan_layers=false \
55-
--lazy_load_tensors=False
93+
--lazy_load_tensors=False \
94+
--eager_load_method="transformers"
5695
```
5796

58-
For the Llama4 model family, we are using a separate checkpoint conversion script (of note, we will gradually migrate all checkpoint conversion scripts to the above consolidated tool soon):
97+
After conversion finishes, set `MAXTEXT_CKPT_PATH` to the converted MaxText checkpoint path.
5998

60-
```shell
61-
export LOCAL_HF_MODEL_PATH=... # Need to pre-download the safetensors from HuggingFace
62-
export MAXTEXT_CKPT_PATH=<Checkpoint GCS path> # gs://my-bucket/path
63-
python -m maxtext.checkpoint_conversion.standalone_scripts.llama4_ckpt_unscanned \
64-
--model-size=llama4-17b-16e \
65-
--huggingface-checkpoint=True \
66-
--base-model-path=${LOCAL_HF_MODEL_PATH?} \
67-
--maxtext-model-path=${MAXTEXT_CKPT_PATH?}
99+
```sh
100+
export MAXTEXT_CKPT_PATH=<CKPT_PATH> # e.g., gs://my-bucket/my-model-checkpoint/0/items
68101
```
69102

103+
> [!IMPORTANT]
104+
> **Matching the `scan_layers` Parameter:**
105+
> The `scan_layers` setting during your fine-tuning run **must match** the setting used when creating the checkpoint at `MAXTEXT_CKPT_PATH`.
106+
>
107+
> - If the checkpoint was converted or saved with `scan_layers=False` (which is common for Hugging Face conversions and inference-ready models), you **must also provide `scan_layers=False` in the MaxText command.**
108+
70109
## Multimodal Decode
71110

72111
MaxText supports multimodal decoding, allowing you to input text with multiple images to get a text output. To use this feature, you need three main settings:
@@ -77,23 +116,18 @@ MaxText supports multimodal decoding, allowing you to input text with multiple i
77116

78117
Since each model uses a unique native chatting template from its pretraining, we've implemented these specific templates within `multimodal_utils.py` and apply them directly to your prompt.
79118

80-
### Decode with text+image
119+
### Decode with Text + Image
81120

82121
To run a forward pass and verify the model's output, use the following command:
83122

84-
```shell
85-
# Gemma3 decode
86-
python -m maxtext.inference.decode \
87-
model_name=gemma3-4b \
88-
hf_access_token=${HF_TOKEN?} \
123+
```sh
124+
python3 -m maxtext.inference.decode \
125+
model_name=${MODEL?} \
89126
tokenizer_path=src/maxtext/assets/tokenizers/tokenizer.gemma3 \
90-
load_parameters_path=${MAXTEXT_CKPT_PATH?}/0/items \
127+
load_parameters_path=${MAXTEXT_CKPT_PATH?} \
91128
per_device_batch_size=1 \
92-
run_name=ht_test \
93129
max_prefill_predict_length=272 \
94130
max_target_length=300 \
95-
steps=1 \
96-
async_checkpointing=false \
97131
scan_layers=false \
98132
use_multimodal=true \
99133
prompt='Describe image <start_of_image>' \
@@ -109,7 +143,7 @@ Describe image <start_of_image><end_of_turn>
109143
<start_of_turn>model
110144
` -> `Here's a description of the image:
111145
112-
**Overall Impression:** The image is a bright, expansive cityscape view of Seattle, Washington, with`
146+
**Overall Impression:** The image is a bright, expansive cityscape view of Seattle, Washington,`
113147
```
114148

115149
To decode with multiple images at once, you can provide multiple image paths like this:
@@ -118,8 +152,8 @@ To decode with multiple images at once, you can provide multiple image paths lik
118152
export TARGET_LENGTH=... # Adjust to fit expected output length
119153
export PREDICT_LENGTH=... # Adjust to fit image tokens + text prompt
120154
121-
python -m maxtext.inference.decode \
122-
model_name=gemma3-4b \
155+
python3 -m maxtext.inference.decode \
156+
model_name=${MODEL?} \
123157
... \
124158
max_prefill_predict_length=${PREDICT_LENGTH?} # Adjust to fit image tokens + text prompt \
125159
max_target_length=${TARGET_LENGTH?} \
@@ -130,22 +164,19 @@ python -m maxtext.inference.decode \
130164

131165
For larger models such as Llama4-Scout/Maverick, we suggest to run the decoding on a TPU cluster such as v5p-16.
132166

133-
### Decode with text+video+audio
167+
### Decode with Text + Video + Audio
134168

135169
For models that support video input (e.g., Qwen3-Omni and Qwen3.5), pass a video file via `video_path`. For Qwen3-Omni, which also supports audio, set `use_audio_in_video=true` to additionally process the embedded audio track. Since the required token budget scales with video length and resolution, set `max_prefill_predict_length` accordingly.
136170

137-
```shell
171+
```sh
138172
# Qwen3-Omni decode with video + audio
139-
export MAXTEXT_CKPT_PATH=<Checkpoint GCS path> # gs://my-bucket/path for Qwen3-Omni
140-
python -m maxtext.inference.decode \
173+
export MAXTEXT_CKPT_PATH=<Checkpoint GCS path> # gs://my-bucket/path/0/items for Qwen3-Omni
174+
python3 -m maxtext.inference.decode \
141175
model_name=qwen3-omni-30b-a3b \
142176
tokenizer_path=Qwen/Qwen3-Omni-30B-A3B-Instruct \
143177
tokenizer_type=huggingface \
144-
load_parameters_path=${MAXTEXT_CKPT_PATH?}/0/items \
178+
load_parameters_path=${MAXTEXT_CKPT_PATH?} \
145179
per_device_batch_size=1 \
146-
run_name=ht_test \
147-
steps=1 \
148-
async_checkpointing=false \
149180
scan_layers=false \
150181
use_multimodal=true \
151182
use_audio_in_video=true \
@@ -173,15 +204,10 @@ Supervised Fine-Tuning (SFT) of multimodal LLMs in MaxText focuses specifically
173204
**Text+image SFT is supported for all models listed above.** The following example uses Gemma3-4B with the [ChartQA](https://huggingface.co/datasets/HuggingFaceM4/ChartQA) dataset:
174205

175206
```shell
176-
export MAXTEXT_CKPT_PATH=<your-checkpoints-path> # either set to an already available MaxText ckpt or to the one we just converted in the previous step
177-
export BASE_OUTPUT_DIRECTORY=gs://<GCS_BUCKET>
178-
export STEPS=1000
179-
python -m maxtext.trainers.post_train.sft.train_sft_native \
207+
python3 -m maxtext.trainers.post_train.sft.train_sft_native \
180208
src/maxtext/configs/post_train/sft-vision-chartqa.yml \
181-
run_name="chartqa-sft" \
182-
model_name=gemma3-4b \
183-
tokenizer_path="google/gemma-3-4b-it" \
184-
hf_access_token=${HF_TOKEN?} \
209+
run_name=${RUN_NAME?} \
210+
model_name=${MODEL?} \
185211
load_parameters_path=${MAXTEXT_CKPT_PATH?} \
186212
base_output_directory=${BASE_OUTPUT_DIRECTORY?} \
187213
per_device_batch_size=1 \

src/maxtext/trainers/post_train/sft/train_sft_native.py

Lines changed: 7 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -14,182 +14,27 @@
1414

1515
"Training loop for Supervised Fine-Tuning (SFT)."
1616

17-
import datetime
18-
import os
1917
from typing import Sequence
2018

2119
from absl import app
2220

23-
import numpy as np
24-
25-
import tensorflow as tf
26-
import jax
27-
28-
from flax.linen import partitioning as nn_partitioning
29-
30-
from maxtext.configs import pyconfig
31-
from maxtext.trainers.pre_train.train import (
32-
eval_step,
33-
get_first_step,
34-
train_step,
35-
)
36-
from maxtext.common import checkpointing, profiler
37-
from maxtext.common.data_loader import DataLoader
3821
from maxtext.common.goodput import (
39-
GoodputEvent,
40-
RECORD_JOB_END_TIME,
4122
RECORD_JOB_START_TIME,
42-
create_goodput_recorder,
4323
maybe_monitor_goodput,
44-
maybe_record_goodput,
4524
record_goodput,
4625
)
47-
from maxtext.common.metric_logger import MetricLogger
48-
from maxtext.utils import exceptions
49-
from maxtext.utils import gcs_utils
50-
from maxtext.utils import max_utils
51-
from maxtext.utils import max_logging
52-
from maxtext.utils import maxtext_utils
53-
from maxtext.utils import sharding
54-
from maxtext.utils import train_utils
55-
56-
57-
def train_loop(config, recorder, state=None):
58-
"""Main training loop for SFT."""
59-
if not config.use_sft:
60-
raise TypeError("Set use_sft to True to run Supervised Fine Tuning.")
61-
62-
(
63-
init_rng,
64-
checkpoint_manager,
65-
state_mesh_shardings,
66-
model,
67-
mesh,
68-
learning_rate_schedule,
69-
data_iterator,
70-
_,
71-
_,
72-
eval_data_iterator,
73-
state,
74-
) = train_utils.setup_train_loop(config, recorder)
75-
76-
params_shardings, state_mesh_shardings = sharding.maybe_update_params_sharding_with_opt(config, state_mesh_shardings)
77-
78-
p_train_step, p_eval_step = train_utils.jit_train_and_eval_step(
79-
config, model, mesh, state, state_mesh_shardings, train_step, eval_step, eval_data_iterator, params_shardings
80-
)
81-
82-
with jax.set_mesh(mesh), nn_partitioning.axis_rules(config.logical_axis_rules):
83-
data_sharding = sharding.get_input_data_sharding(config, mesh)
84-
shaped_batch = maxtext_utils.get_shaped_batch(config, batch_sharding=data_sharding)
85-
compiled = p_train_step.lower(state, shaped_batch, init_rng).compile()
86-
compiled_stats = compiled.memory_analysis()
87-
max_utils.print_compiled_memory_stats(compiled_stats)
88-
89-
start_step = get_first_step(model, state) # this is the start_step for training
90-
prof = profiler.Profiler(config, offset_step=start_step)
91-
data_loader = DataLoader(config, mesh, data_iterator, recorder)
92-
metric_logger = MetricLogger(config=config, learning_rate_schedule=learning_rate_schedule)
93-
94-
# Write train config params, num model params, and XLA flags to tensorboard
95-
metric_logger.write_setup_info_to_tensorboard(state.params)
96-
97-
_job_completed_gracefully = False
98-
try:
99-
last_step_completion = datetime.datetime.now()
100-
for step in np.arange(start_step, config.steps):
101-
prof.maybe_activate_profiler(step, state)
102-
103-
with jax.profiler.StepTraceAnnotation("train", step_num=step):
104-
example_batch = data_loader.load_next_batch()
105-
# pylint: disable=not-callable
106-
nextrng = jax.jit(jax.random.fold_in)(init_rng, step)
107-
with maybe_record_goodput(recorder, GoodputEvent.STEP, step):
108-
with jax.set_mesh(mesh), nn_partitioning.axis_rules(config.logical_axis_rules):
109-
state, metrics = p_train_step(state, example_batch, nextrng)
110-
111-
step_time_delta = datetime.datetime.now() - last_step_completion
112-
113-
checkpointing.maybe_save_checkpoint(checkpoint_manager, state, config, data_iterator, step)
114-
115-
if config.dump_hlo and step == start_step:
116-
jax.block_until_ready(state) # Ensure compilation has finished.
117-
gcs_utils.upload_dump(
118-
config.dump_hlo_local_dir,
119-
config.dump_hlo_gcs_dir,
120-
module_name=config.dump_hlo_module_name,
121-
delete_local_after=config.dump_hlo_delete_local_after,
122-
all_host_upload=config.dump_hlo_upload_all,
123-
)
124-
125-
if config.eval_interval > 0 and step > start_step and (step + 1) % config.eval_interval == 0:
126-
assert eval_data_iterator
127-
# Explicitly reset the eval iterator and counters before starting the eval loop
128-
eval_data_iterator.reset()
129-
metric_logger.reset_eval_metrics()
130-
max_logging.log(f"Starting eval after train step {step}")
131-
eval_step_count = 0
132-
last_eval_step_completion = datetime.datetime.now()
133-
# pylint: disable=not-callable
134-
for eval_batch in eval_data_iterator:
135-
if config.eval_steps > 0 and eval_step_count >= config.eval_steps:
136-
break
137-
with jax.set_mesh(mesh), nn_partitioning.axis_rules(config.logical_axis_rules):
138-
eval_metrics = p_eval_step(state, eval_batch, nextrng)
139-
eval_step_time_delta = datetime.datetime.now() - last_eval_step_completion
140-
last_eval_step_completion = datetime.datetime.now()
141-
metric_logger.buffer_and_write_metrics(
142-
eval_metrics, eval_step_count, step_time_delta=eval_step_time_delta, is_training=False
143-
)
144-
max_logging.log(f"Completed eval step {eval_step_count}")
145-
eval_step_count += 1
146-
147-
prof.maybe_deactivate_profiler(step, state)
148-
149-
if step == start_step:
150-
max_utils.print_mem_stats("After params initialized")
151-
152-
last_step_completion = datetime.datetime.now()
153-
metric_logger.buffer_and_write_metrics(metrics, step, step_time_delta)
154-
155-
if config.save_checkpoint_on_completion:
156-
checkpointing.maybe_save_checkpoint(checkpoint_manager, state, config, data_iterator)
157-
elif checkpoint_manager is not None:
158-
# in case the last checkpoint_period checkpoint is still in progress
159-
checkpoint_manager.wait_until_finished()
160-
_job_completed_gracefully = True
161-
except exceptions.StopTraining as e:
162-
prof.deactivate()
163-
max_logging.log(f"Training stopped: {str(e)}")
164-
_job_completed_gracefully = True
165-
finally:
166-
if _job_completed_gracefully:
167-
record_goodput(recorder, RECORD_JOB_END_TIME)
168-
metric_logger.flush_metrics_and_cleanup()
169-
170-
return state
26+
from maxtext.trainers.pre_train.train import get_train_func, initialize
17127

17228

17329
def main(argv: Sequence[str]) -> None:
174-
jax.config.update("jax_default_prng_impl", "unsafe_rbg")
175-
# TF allocates extraneous GPU memory when using TFDS data
176-
# this leads to CUDA OOMs. WAR for now is to hide GPUs from TF
177-
tf.config.set_visible_devices([], "GPU")
178-
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "0"
179-
if "xla_tpu_spmd_rng_bit_generator_unsafe" not in os.environ.get("LIBTPU_INIT_ARGS", ""):
180-
os.environ["LIBTPU_INIT_ARGS"] = (
181-
os.environ.get("LIBTPU_INIT_ARGS", "") + " --xla_tpu_spmd_rng_bit_generator_unsafe=true"
182-
)
183-
config = pyconfig.initialize(argv, use_tunix_gradient_accumulation=False)
184-
jax.config.update("jax_use_shardy_partitioner", config.shardy)
185-
max_utils.print_system_information()
186-
train_utils.validate_train_config(config)
187-
os.environ["TFDS_DATA_DIR"] = config.dataset_path
188-
189-
recorder = create_goodput_recorder(config)
30+
argv = list(argv)
31+
argv.append("use_sft=True")
32+
argv.append("use_tunix_gradient_accumulation=False")
33+
config, recorder = initialize(argv)
19034
record_goodput(recorder, RECORD_JOB_START_TIME)
35+
train_func = get_train_func(config, recorder, argv)
19136
with maybe_monitor_goodput(config):
192-
train_loop(config, recorder)
37+
train_func()
19338

19439

19540
if __name__ == "__main__":

0 commit comments

Comments
 (0)