Skip to content

Commit a56dc04

Browse files
Add support for On-The-Fly Dynamic SafeTensors loading.
PiperOrigin-RevId: 940210414
1 parent 7d2bd7b commit a56dc04

9 files changed

Lines changed: 1069 additions & 43 deletions

File tree

src/maxtext/checkpoint_conversion/to_maxtext.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@
6767
from maxtext.common.common_types import MODEL_MODE_TRAIN
6868
from maxtext.checkpoint_conversion.utils.hf_model_configs import HF_MODEL_CONFIGS
6969
from maxtext.checkpoint_conversion.utils.param_mapping import HOOK_FNS, PARAM_MAPPING
70-
from maxtext.checkpoint_conversion.utils.utils import MemoryMonitorTqdm, apply_hook_fns, load_hf_dict_from_transformers, load_hf_dict_from_safetensors, param_key_parts_from_path, print_peak_memory, print_ram_usage, save_weights_to_checkpoint, validate_and_filter_param_map_keys
70+
from maxtext.checkpoint_conversion.utils.tensor_handling import apply_hook_fns
71+
from maxtext.checkpoint_conversion.utils.utils import MemoryMonitorTqdm, load_hf_dict_from_transformers, load_hf_dict_from_safetensors, param_key_parts_from_path, print_peak_memory, print_ram_usage, save_weights_to_checkpoint, validate_and_filter_param_map_keys
7172
from maxtext.inference.inference_utils import str2bool
7273
from maxtext.layers import quantizations
7374
from maxtext.models import models
@@ -319,11 +320,9 @@ def get_maxtext_model_info(config):
319320
# Get abstract model structure (name, shape) without materializing the weights to save memory
320321
abstract_params_tree = maxtext_utils.get_abstract_param(maxtext_model_flax, config)["params"]
321322

322-
abstract_params_flat, abstract_params_treedef = (
323-
jax.tree_util.tree_flatten_with_path(
324-
abstract_params_tree,
325-
is_leaf=lambda x: isinstance(x, nn.LogicallyPartitioned),
326-
)
323+
abstract_params_flat, abstract_params_treedef = jax.tree_util.tree_flatten_with_path(
324+
abstract_params_tree,
325+
is_leaf=lambda x: isinstance(x, nn.LogicallyPartitioned),
327326
)
328327

329328
max_logging.log("MaxText abstract model and state initialized.")
Lines changed: 371 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,371 @@
1+
# Copyright 2023–2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Dynamic loading of HuggingFace checkpoints during training/eval workloads directly in the target format.
16+
17+
This module allows loading HuggingFace checkpoints (in Safetensors format)
18+
directly during MaxText training or evaluation runs, performing on-the-fly sharded
19+
restore and CPU/TPU transformations. This avoids offline pre-conversion steps
20+
and prevents host OOM.
21+
22+
Usage:
23+
To load Hugging Face checkpoints directly, configure the following flags:
24+
1. `source_checkpoint_layout`: Set to `"safetensors_dynamic"`.
25+
2. `load_parameters_path`: Set to the source path of the Hugging Face checkpoint.
26+
27+
Examples:
28+
A. Load from a Google Cloud Storage (GCS) directory containing `.safetensors`:
29+
```
30+
python3 maxtext/trainers/pre_train/train.py \
31+
maxtext/configs/base.yml \
32+
run_name=my_run \
33+
model_name=llama3.1-8b \
34+
source_checkpoint_layout="safetensors_dynamic" \
35+
load_parameters_path="gs://my-bucket/path/to/safetensors_directory/"
36+
```
37+
38+
B. Load directly from the Hugging Face Hub (automatically cached to GCS):
39+
```
40+
python3 maxtext/trainers/pre_train/train.py \
41+
maxtext/configs/base.yml \
42+
run_name=my_run \
43+
model_name=llama3.1-8b \
44+
source_checkpoint_layout="safetensors_dynamic" \
45+
load_parameters_path="hf://meta-llama/Meta-Llama-3-8B" \
46+
hf_access_token="<your_token>" \
47+
base_output_directory="gs://my-bucket/output/"
48+
```
49+
50+
C. Load from Hugging Face Hub using automatic model_name resolution:
51+
```
52+
python3 maxtext/trainers/pre_train/train.py \
53+
maxtext/configs/base.yml \
54+
run_name=my_run \
55+
model_name=llama3.1-8b \
56+
source_checkpoint_layout="safetensors_dynamic" \
57+
load_parameters_path="" \
58+
hf_access_token="<your_token>" \
59+
base_output_directory="gs://my-bucket/output/"
60+
```
61+
62+
Note:
63+
- Hugging Face weights from HF Hub are cached to `base_output_directory`.
64+
- When loading from Hugging Face Hub, `base_output_directory` must start with
65+
"gs://" and `hf_access_token` is required if downloading gated models.
66+
"""
67+
68+
import concurrent.futures
69+
import multiprocessing
70+
import os
71+
import random
72+
import time
73+
74+
from flax import nnx
75+
import flax.traverse_util
76+
from google.cloud import storage
77+
import huggingface_hub
78+
import jax
79+
from maxtext.checkpoint_conversion.utils import hf_model_configs
80+
from maxtext.checkpoint_conversion.utils import param_mapping
81+
from maxtext.checkpoint_conversion.utils import tensor_handling
82+
from maxtext.utils import gcs_utils
83+
from maxtext.utils import globals as maxtext_globals
84+
from maxtext.utils import max_logging
85+
from orbax.checkpoint import v1 as ocp_v1
86+
from orbax.checkpoint._src.arrays import sharding as sharding_utils
87+
88+
89+
HF_MODEL_CONFIGS = hf_model_configs.HF_MODEL_CONFIGS
90+
get_hf_loading_function = tensor_handling.get_hf_loading_function
91+
92+
93+
def build_gcs_cache_worker(fpath, gcs_cache_dir, hf_access_token):
94+
"""Caches a file from Hugging Face to a GCS bucket cache directory.
95+
96+
Args:
97+
fpath: The full remote file path on the Hugging Face virtual file system
98+
(e.g., "meta-llama/Meta-Llama-3-8B/model-00001-of-00004.safetensors").
99+
gcs_cache_dir: The destination directory in GCS.
100+
hf_access_token: The access token for Hugging Face.
101+
"""
102+
fs = huggingface_hub.HfFileSystem(token=hf_access_token)
103+
time.sleep(random.uniform(0.0, 5.0))
104+
105+
bucket_name, blob_prefix = gcs_utils.parse_gcs_bucket_and_prefix(gcs_cache_dir)
106+
blob_name = os.path.join(blob_prefix, os.path.basename(fpath))
107+
108+
storage_client = storage.Client()
109+
bucket = storage_client.bucket(bucket_name)
110+
blob = bucket.blob(blob_name)
111+
112+
if blob.exists():
113+
max_logging.log(f"[Worker] Cache hit for {os.path.basename(fpath)}.")
114+
return
115+
116+
t0 = time.time()
117+
max_retries = 5
118+
for attempt in range(max_retries):
119+
try:
120+
with fs.open(fpath, "rb") as remote_f:
121+
blob.chunk_size = 1024 * 1024 * 32 # 32MB chunks
122+
blob.upload_from_file(remote_f, client=storage_client)
123+
print(
124+
f"[Worker] Cached {os.path.basename(fpath)} in" f" {time.time() - t0:.1f}s",
125+
flush=True,
126+
)
127+
break
128+
except Exception as e: # pylint: disable=broad-exception-caught
129+
if attempt < max_retries - 1:
130+
max_logging.log(
131+
f"Error fetching {fpath} to GCS: {e}. Retrying in 15 seconds..." f" (Attempt {attempt+1}/{max_retries})"
132+
)
133+
time.sleep(15)
134+
else:
135+
max_logging.log(f"Failed to fetch {fpath} to GCS after {max_retries} attempts.")
136+
raise
137+
138+
139+
def get_hf_config_and_mappings(maxtext_config):
140+
"""Gets HF config and parameter mapping based on the MaxText config."""
141+
model_key = maxtext_config.model_name
142+
if "-Instruct" in model_key:
143+
model_key = model_key.replace("-Instruct", "")
144+
hf_config_obj = HF_MODEL_CONFIGS[model_key]
145+
hf_config_dict = hf_config_obj.to_dict()
146+
147+
param_map_mt_to_hf = param_mapping.PARAM_MAPPING[model_key](
148+
hf_config_dict, maxtext_config, scan_layers=maxtext_config.scan_layers
149+
)
150+
hook_fn_map_mt = param_mapping.HOOK_FNS[model_key](
151+
hf_config_dict, maxtext_config, scan_layers=maxtext_config.scan_layers, saving_to_hf=False
152+
)
153+
return param_map_mt_to_hf, hook_fn_map_mt
154+
155+
156+
def load_sharded_hf_state(path):
157+
"""Loads HF state with maximal sharding across TPU mesh to avoid host OOM.
158+
159+
Args:
160+
path: A directory path (either local or GCS starting with gs://) containing
161+
the .safetensors files (e.g., "gs://my-bucket/hf_cache/model_id" or
162+
"/path/to/safetensors_directory/"). If a Hugging Face Hub ID was used,
163+
it should already be cached/downloaded to GCS before calling this
164+
function.
165+
166+
Returns:
167+
The loaded Hugging Face state dictionary mapping parameter names to
168+
JAX arrays.
169+
"""
170+
t0 = time.time()
171+
context = ocp_v1.Context(checkpoint_layout=ocp_v1.options.CheckpointLayout.SAFETENSORS)
172+
with context:
173+
metadata = ocp_v1.pytree_metadata(path)
174+
simple_abstract_state = metadata.metadata
175+
176+
# Distributed Sharded Download: Tell JAX to shard the HF Safetensors download
177+
# across the entire TPU mesh to avoid Host OOM.
178+
current_global_devices = jax.devices()
179+
shardings = sharding_utils.construct_maximal_shardings(simple_abstract_state, devices=current_global_devices)
180+
181+
def combine_sharding(sds, single_sharding):
182+
return jax.ShapeDtypeStruct(shape=sds.shape, dtype=sds.dtype, sharding=single_sharding)
183+
184+
sharded_abstract_state = jax.tree.map(combine_sharding, simple_abstract_state, shardings)
185+
186+
max_logging.log("Reading raw Safetensors into memory (Distributed Sharded GCS Download)...")
187+
hf_state = ocp_v1.load_pytree(path, sharded_abstract_state)
188+
max_logging.log(f"load_sharded_hf_state took {time.time() - t0:.2f}s")
189+
return hf_state
190+
191+
192+
def transform_hf_state_to_mt_state(hf_state, target_tree, param_map_mt_to_hf, hook_fn_map_mt, maxtext_config):
193+
"""Transforms HF state into MaxText state by applying param mappings and mathematical hooks."""
194+
t0 = time.time()
195+
196+
def tensor_getter(key):
197+
return hf_state.pop(key)
198+
199+
flat_target = flax.traverse_util.flatten_dict(target_tree, sep=".")
200+
flat_restored = flat_target.copy()
201+
202+
mapped_count = 0
203+
keys_missed = []
204+
max_logging.log("Starting fast in-memory Distributed Transformations...")
205+
206+
for mt_key, hf_source in param_map_mt_to_hf.items():
207+
mt_name = mt_key.replace("params-", "").replace("-", ".")
208+
209+
# Determine the correct key in flat_target
210+
check_name = mt_name
211+
if check_name not in flat_target:
212+
if f"params.{mt_name}" in flat_target:
213+
check_name = f"params.{mt_name}"
214+
elif mt_key.replace("-", ".") in flat_target:
215+
check_name = mt_key.replace("-", ".")
216+
217+
if check_name not in flat_target:
218+
keys_missed.append(mt_name)
219+
continue
220+
221+
target_leaf = flat_target[check_name]
222+
hook_fn = hook_fn_map_mt.get(mt_key)
223+
224+
load_fn = get_hf_loading_function(
225+
hf_source,
226+
tensor_getter,
227+
hook_fn,
228+
target_leaf,
229+
maxtext_config,
230+
)
231+
232+
# Execute transformation and assign to flat_restored
233+
t_layer = time.time()
234+
flat_restored[check_name] = load_fn()
235+
236+
max_logging.log(f"Transformed {check_name} from {hf_source} in {time.time() - t_layer:.4f}s")
237+
mapped_count += 1
238+
239+
if mapped_count == 0:
240+
max_logging.log(f"All transformations missed! Sample missed mt_names: {keys_missed[:5]}")
241+
max_logging.log(f"Sample flat_target keys: {list(flat_target.keys())[:5]}")
242+
243+
max_logging.log(f"Successfully mapped {mapped_count} parameters.")
244+
restored_params = flax.traverse_util.unflatten_dict(flat_restored, sep=".")
245+
246+
if "params" in restored_params:
247+
restored_params = restored_params["params"]
248+
249+
max_logging.log(f"transform_hf_state_to_mt_state took {time.time() - t0:.2f}s")
250+
251+
return {"params": restored_params}
252+
253+
254+
def load_safetensors_dynamic_state(path, abstract_unboxed_pre_state, maxtext_config):
255+
"""Main entry point to dynamically build and load safetensors into MaxText format.
256+
257+
Splits execution into:
258+
1. Deriving Mappings
259+
2. Loading Sharded arrays directly to TPUs
260+
3. Processing the transformations natively on TPUs
261+
"""
262+
if maxtext_config is None:
263+
raise ValueError("maxtext_config must be provided for safetensors_dynamic loading.")
264+
265+
model_name = maxtext_config.model_name
266+
if "-Instruct" in model_name:
267+
model_name = model_name.replace("-Instruct", "")
268+
269+
if not path:
270+
if model_name not in maxtext_globals.HF_IDS:
271+
raise ValueError(f"Unsupported model name for automatic HF repo resolution: {model_name}.")
272+
path = maxtext_globals.HF_IDS[model_name]
273+
274+
if path.startswith("hf://"):
275+
path = path[5:]
276+
277+
if not path.startswith("gs://") and not os.path.isdir(path):
278+
fs = huggingface_hub.HfFileSystem(token=maxtext_config.hf_access_token)
279+
repo_id = path
280+
281+
files = fs.glob(f"{repo_id}/*.safetensors")
282+
283+
host_id = jax.process_index()
284+
285+
if hasattr(maxtext_config, "base_output_directory") and maxtext_config.base_output_directory.startswith("gs://"):
286+
gcs_cache_dir = f"{maxtext_config.base_output_directory}/hf_cache/{repo_id.replace('/', '_')}"
287+
path = gcs_cache_dir
288+
289+
# Only Host 0 downloads to the shared GCS cache
290+
if host_id == 0:
291+
max_logging.log("Dynamic HF Hub Fast DL: Host 0 is downloading to shared GCS" f" Cache: {gcs_cache_dir}")
292+
t_gcs_start = time.time()
293+
294+
# List existing blobs to avoid spawning processes for already cached
295+
# files
296+
storage_client = storage.Client()
297+
bucket_name = gcs_cache_dir.replace("gs://", "").split("/", maxsplit=1)[0]
298+
blob_prefix = (
299+
gcs_cache_dir.replace("gs://", "").split("/", maxsplit=1)[1]
300+
if "/" in gcs_cache_dir.replace("gs://", "")
301+
else ""
302+
)
303+
304+
existing_blobs = {blob.name for blob in storage_client.list_blobs(bucket_name, prefix=blob_prefix)}
305+
306+
files_to_download = []
307+
for fpath in files:
308+
expected_blob_name = os.path.join(blob_prefix, os.path.basename(fpath))
309+
if expected_blob_name not in existing_blobs:
310+
files_to_download.append(fpath)
311+
312+
if files_to_download:
313+
with concurrent.futures.ProcessPoolExecutor(
314+
max_workers=32, mp_context=multiprocessing.get_context("spawn")
315+
) as executor:
316+
futures = [
317+
executor.submit(
318+
build_gcs_cache_worker,
319+
fpath,
320+
gcs_cache_dir,
321+
maxtext_config.hf_access_token,
322+
)
323+
for fpath in files_to_download
324+
]
325+
326+
while futures:
327+
done, futures = concurrent.futures.wait(futures, timeout=10)
328+
329+
# Raise any exceptions if a worker failed
330+
for f in done:
331+
f.result()
332+
333+
t_gcs_end = time.time()
334+
max_logging.log(
335+
f"GCS caching complete in {t_gcs_end - t_gcs_start:.2f}s."
336+
f" Downloaded {len(files_to_download)} missing files."
337+
)
338+
339+
# Global barrier: all hosts wait for Host 0 to finish downloading to the
340+
# shared GCS bucket
341+
max_logging.log(f"Host {host_id} waiting for GCS cache at {gcs_cache_dir} to be" " populated by Host 0...")
342+
jax.experimental.multihost_utils.sync_global_devices("dynamic_hf_download_complete")
343+
max_logging.log(f"Host {host_id} detected GCS cache is ready!")
344+
345+
else:
346+
raise ValueError("base_output_directory with gs:// prefix is required for " "huggingface downloads.")
347+
348+
t_total = time.time()
349+
param_map_mt_to_hf, hook_fn_map_mt = get_hf_config_and_mappings(maxtext_config)
350+
max_logging.log(f"[1/3] Mappings derived in {time.time() - t_total:.2f}s")
351+
352+
target_tree = (
353+
abstract_unboxed_pre_state.to_pure_dict()
354+
if isinstance(abstract_unboxed_pre_state, nnx.State)
355+
else abstract_unboxed_pre_state.params
356+
)
357+
358+
t1 = time.time()
359+
hf_state = load_sharded_hf_state(path)
360+
max_logging.log(f"[2/3] Distributed Sharded GCS load completed in {time.time() - t1:.2f}s")
361+
362+
t2 = time.time()
363+
# Transform Hugging Face weight tensors on-the-fly into MaxText format
364+
# in-memory. This is done in-memory on each host, sharded across the mesh.
365+
restored_params = transform_hf_state_to_mt_state(
366+
hf_state, target_tree, param_map_mt_to_hf, hook_fn_map_mt, maxtext_config
367+
)
368+
max_logging.log(f"[3/3] CPU Transformations completed in {time.time() - t2:.2f}s")
369+
max_logging.log(f"Total safetensors_dynamic duration: {time.time() - t_total:.2f}s")
370+
371+
return None, restored_params

0 commit comments

Comments
 (0)