Bug report
save_weights_to_checkpoint in src/maxtext/checkpoint_conversion/utils/utils.py fails deterministically when lazily-loaded tensors are combined with a single simulated device. No checkpoint is produced, so every conversion using that configuration is blocked.
Steps to reproduce
On any machine that can run MaxText checkpoint conversion (the failing step is CPU-only; we reproduced it on an 8xH100 x86_64 node and on aarch64 GB200/GB300 nodes, but no accelerator is required to hit it):
- Install MaxText at
main (2026-09-02 or later) with orbax-checkpoint 0.12.4.
- Fetch any supported Hugging Face checkpoint, e.g.
meta-llama/Llama-3.1-8B-Instruct.
- Run a
checkpoint_conversion entry point that calls save_weights_to_checkpoint, with lazy loading on and exactly one simulated device:
JAX_PLATFORMS=cpu python3 -m maxtext.checkpoint_conversion.to_maxtext \
src/MaxText/configs/base.yml \
model_name=llama3.1-8b \
base_output_directory=/tmp/llama31-8b-orbax \
--hf_model_path=/path/to/Llama-3.1-8B-Instruct \
--lazy_load_tensors=true \
--simulated_cpu_devices_count=1 \
--save_dtype=bfloat16
The default --simulated_cpu_devices_count=16 hides the bug, because it routes through the sharding path. Setting it to 1 is the only change needed to trigger the failure.
Expected behavior
The conversion completes and writes the converted Orbax checkpoint to base_output_directory, exactly as it does today with --simulated_cpu_devices_count > 1 and exactly as it did before common/checkpointing.py moved to the Orbax v1 API. --simulated_cpu_devices_count is documented as controlling how many CPU devices are simulated for sharding; it is not expected to change whether a checkpoint can be saved at all. The run should log Conversion complete. Checkpoint saved to ... and exit 0, and the resulting checkpoint should be byte-equivalent in content to one produced with more simulated devices, since the single-device path differs only in that it skips sharding.
Actual behavior
The save aborts with an Orbax NoEntryError raised from resolve_handler_for_save while resolving a handler for the "items" checkpointable, before any file is written. The process exits non-zero and base_output_directory is left empty. In our runs this happened about 15 seconds into the conversion step, deterministically, on every attempt and on both x86_64 and aarch64.
Root cause
The lazy proxies are never materialized on the single-device path:
to_maxtext.py places LazyTensor proxies as weight-tree leaves under --lazy_load_tensors, and registers a handler for them through the Orbax v0 API (orbax.checkpoint.type_handlers.register_type_handler).
- The only surviving code that converts those proxies to NumPy is inside
shard_jax_weights (if not isinstance(arr, (np.ndarray, jax.Array)): arr = np.array(arr)).
save_weights_to_checkpoint skips shard_jax_weights when device_count == 1 (logging Single device: Skip sharding), so unmaterialized proxies end up in the TrainState passed to save_checkpoint.
common/checkpointing.py now uses Orbax v1 (from orbax.checkpoint import v1 as ocp) and calls save_checkpointables with {"items": state}. Orbax v1 resolves a handler by checkpointable name or handler.is_handleable(value), never by the value's top-level type. "items" is unbound in the default global registry, so PyTreeHandler.is_handleable tree-maps over the leaves against StandardLeafHandlerRegistry (jax.Array, np.ndarray, int, float, bytes, str) with issubclass. LazyTensor is a plain proxy implementing __array__ and is not an np.ndarray subclass, so no leaf handler matches. The underlying UnregisteredTypeError is swallowed by a bare except Exception and re-surfaces as an opaque NoEntryError.
The Orbax v0 and v1 leaf registries are disjoint, so the v1 migration of common/checkpointing.py orphaned the v0 LazyTensor registration in to_maxtext.py — the registration still runs (it still logs its np.ndarray collision warning) but is invisible to the v1 save path.
Suggested fix
Materialize the lazy leaves on the single-device branch, reusing the conversion shard_jax_weights already performs. We validated this experimentally as an in-image patch on 8xH100 with Llama-3.1-8B: the previously-failing conversion completes, the checkpoint is written, downstream decode passes, and inference throughput and step times are unchanged within run-to-run noise (about 0.1-0.4%) against a pre-regression baseline. The diff is below.
--- a/src/maxtext/checkpoint_conversion/utils/utils.py
+++ b/src/maxtext/checkpoint_conversion/utils/utils.py
@@ def shard_jax_weights(...)
return jax_weights
+def materialize_lazy_weights(jax_weights, mem_info):
+ """Materializes lazily-loaded weight proxies into NumPy arrays."""
+ start = time.time()
+ jax_weights_flat, jax_weights_struct = tree.flatten(jax_weights)
+ del jax_weights
+ gc.collect()
+
+ jax_weights_new = []
+ jax_weights_flat.reverse()
+ num_weights = len(jax_weights_flat)
+ for _ in tqdm(range(num_weights)):
+ jax_weight = jax_weights_flat.pop()
+ if not isinstance(jax_weight, (np.ndarray, jax.Array)):
+ # materialize lazy tensor
+ jax_weight = np.array(jax_weight)
+ jax_weights_new.append(jax_weight)
+ del jax_weight
+ gc.collect()
+ logging.debug("Memory usage: %f GB", mem_info.memory_info().rss / (1024**3))
+
+ jax_weights = tree.unflatten(jax_weights_struct, jax_weights_new)
+ max_logging.log(f"Elapse for lazy weight materialization: {(time.time() - start) / 60:.2f} min")
+
+ return jax_weights
+
+
def save_weights_to_checkpoint(
@@ in save_weights_to_checkpoint
else:
# If number of simulated devices is 1, SKIP sharding and SKIP jax conversion.
max_logging.log("Single device: Skip sharding")
+ # Sharding is what would otherwise materialize lazily-loaded weights, so do
+ # it here: the save path below has no handler for lazy proxy leaves.
+ max_logging.log("Single device: Materializing lazy weights")
+ jax_weights = materialize_lazy_weights(jax_weights, mem_info)
It mirrors shard_jax_weights' reverse-pop plus gc.collect() loop, so peak memory stays roughly one tensor above the converted tree, and checkpoint contents are unchanged: this is the same conversion the multi-device path already performs and the same conversion the pre-migration LazyTensorHandler.serialize performed.
Alternatively, port LazyTensorHandler to an Orbax v1 LeafHandler registered through PyTreeOptions/Context, which would preserve streaming into serialization but pins MaxText to the v1 leaf-handler API and only helps to_maxtext.py.
The blast radius is wider than one entry point: save_weights_to_checkpoint serves several conversion entry points, and any of them would hit this if they produced lazy leaves. Today only to_maxtext.py constructs LazyTensor, so only it is affected.
Logs/Output
Relevant log lines, in order, from a failing conversion run:
Type handler registry overriding type "<class '__main__.LazyTensor'>" collision on np.ndarray
Lazy loading ENABLED
Single device: Skip sharding
followed by:
File ".../checkpoint_conversion/utils/utils.py", line 1302, in save_weights_to_checkpoint
checkpointing.save_checkpoint(checkpoint_manager, step, state_new, config=config)
File ".../common/checkpointing.py", line 992, in save_checkpoint
checkpoint_manager.save_checkpointables(...)
File ".../orbax/checkpoint/experimental/v1/_src/saving/saving.py", line 321, in ...
resolve_handler_for_save(...)
File ".../orbax/checkpoint/experimental/v1/_src/handlers/registration.py", line 623, in resolve_handler_for_save
raise NoEntryError(
No checkpoint files are produced. After applying the suggested fix, the same run instead logs Single device: Materializing lazy weights, then Elapse for lazy weight materialization: 1.04 min, then Conversion complete. Checkpoint saved to ..., and downstream decode succeeds with no traceback anywhere in the log.
Environment Information
- MaxText version / commit:
main as of 2026-09-02. Relevant blobs: src/maxtext/checkpoint_conversion/utils/utils.py = 26677a0, src/maxtext/common/checkpointing.py = a11681b, src/maxtext/checkpoint_conversion/to_maxtext.py = ce2eecb.
- orbax-checkpoint: 0.12.4
- jax: 0.11.2.dev20260901
- Model:
meta-llama/Llama-3.1-8B-Instruct (bf16 safetensors)
- Conversion step: CPU-only (
JAX_PLATFORMS=cpu), --simulated_cpu_devices_count=1, --lazy_load_tensors=true, --save_dtype=bfloat16, async_checkpointing=false
- Operating system / hardware: Linux; reproduced on
x86_64 (8xH100 node) and aarch64 (GB200/GB300 nodes). The failure is platform-independent — it happens before any device work.
Additional Context
Reported by NVIDIA. This is a regression relative to the pre-Orbax-v1 behaviour: the same configuration worked before common/checkpointing.py moved to the v1 API, because the v0 LazyTensorHandler was still consulted at save time. It began failing in our nightly runs on the first day that picked up the v1 migration commit, and the last passing run used MaxText from the day before.
This issue was drafted with assistance from the opus AI model.
Bug report
save_weights_to_checkpointinsrc/maxtext/checkpoint_conversion/utils/utils.pyfails deterministically when lazily-loaded tensors are combined with a single simulated device. No checkpoint is produced, so every conversion using that configuration is blocked.Steps to reproduce
On any machine that can run MaxText checkpoint conversion (the failing step is CPU-only; we reproduced it on an 8xH100 x86_64 node and on aarch64 GB200/GB300 nodes, but no accelerator is required to hit it):
main(2026-09-02 or later) withorbax-checkpoint0.12.4.meta-llama/Llama-3.1-8B-Instruct.checkpoint_conversionentry point that callssave_weights_to_checkpoint, with lazy loading on and exactly one simulated device:The default
--simulated_cpu_devices_count=16hides the bug, because it routes through the sharding path. Setting it to1is the only change needed to trigger the failure.Expected behavior
The conversion completes and writes the converted Orbax checkpoint to
base_output_directory, exactly as it does today with--simulated_cpu_devices_count > 1and exactly as it did beforecommon/checkpointing.pymoved to the Orbax v1 API.--simulated_cpu_devices_countis documented as controlling how many CPU devices are simulated for sharding; it is not expected to change whether a checkpoint can be saved at all. The run should logConversion complete. Checkpoint saved to ...and exit 0, and the resulting checkpoint should be byte-equivalent in content to one produced with more simulated devices, since the single-device path differs only in that it skips sharding.Actual behavior
The save aborts with an Orbax
NoEntryErrorraised fromresolve_handler_for_savewhile resolving a handler for the"items"checkpointable, before any file is written. The process exits non-zero andbase_output_directoryis left empty. In our runs this happened about 15 seconds into the conversion step, deterministically, on every attempt and on both x86_64 and aarch64.Root cause
The lazy proxies are never materialized on the single-device path:
to_maxtext.pyplacesLazyTensorproxies as weight-tree leaves under--lazy_load_tensors, and registers a handler for them through the Orbax v0 API (orbax.checkpoint.type_handlers.register_type_handler).shard_jax_weights(if not isinstance(arr, (np.ndarray, jax.Array)): arr = np.array(arr)).save_weights_to_checkpointskipsshard_jax_weightswhendevice_count == 1(loggingSingle device: Skip sharding), so unmaterialized proxies end up in theTrainStatepassed tosave_checkpoint.common/checkpointing.pynow uses Orbax v1 (from orbax.checkpoint import v1 as ocp) and callssave_checkpointableswith{"items": state}. Orbax v1 resolves a handler by checkpointable name orhandler.is_handleable(value), never by the value's top-level type."items"is unbound in the default global registry, soPyTreeHandler.is_handleabletree-maps over the leaves againstStandardLeafHandlerRegistry(jax.Array,np.ndarray,int,float,bytes,str) withissubclass.LazyTensoris a plain proxy implementing__array__and is not annp.ndarraysubclass, so no leaf handler matches. The underlyingUnregisteredTypeErroris swallowed by a bareexcept Exceptionand re-surfaces as an opaqueNoEntryError.The Orbax v0 and v1 leaf registries are disjoint, so the v1 migration of
common/checkpointing.pyorphaned the v0LazyTensorregistration into_maxtext.py— the registration still runs (it still logs itsnp.ndarraycollision warning) but is invisible to the v1 save path.Suggested fix
Materialize the lazy leaves on the single-device branch, reusing the conversion
shard_jax_weightsalready performs. We validated this experimentally as an in-image patch on 8xH100 with Llama-3.1-8B: the previously-failing conversion completes, the checkpoint is written, downstream decode passes, and inference throughput and step times are unchanged within run-to-run noise (about 0.1-0.4%) against a pre-regression baseline. The diff is below.It mirrors
shard_jax_weights' reverse-pop plusgc.collect()loop, so peak memory stays roughly one tensor above the converted tree, and checkpoint contents are unchanged: this is the same conversion the multi-device path already performs and the same conversion the pre-migrationLazyTensorHandler.serializeperformed.Alternatively, port
LazyTensorHandlerto an Orbax v1LeafHandlerregistered throughPyTreeOptions/Context, which would preserve streaming into serialization but pins MaxText to the v1 leaf-handler API and only helpsto_maxtext.py.The blast radius is wider than one entry point:
save_weights_to_checkpointserves several conversion entry points, and any of them would hit this if they produced lazy leaves. Today onlyto_maxtext.pyconstructsLazyTensor, so only it is affected.Logs/Output
Relevant log lines, in order, from a failing conversion run:
followed by:
No checkpoint files are produced. After applying the suggested fix, the same run instead logs
Single device: Materializing lazy weights, thenElapse for lazy weight materialization: 1.04 min, thenConversion complete. Checkpoint saved to ..., and downstream decode succeeds with no traceback anywhere in the log.Environment Information
mainas of 2026-09-02. Relevant blobs:src/maxtext/checkpoint_conversion/utils/utils.py=26677a0,src/maxtext/common/checkpointing.py=a11681b,src/maxtext/checkpoint_conversion/to_maxtext.py=ce2eecb.meta-llama/Llama-3.1-8B-Instruct(bf16 safetensors)JAX_PLATFORMS=cpu),--simulated_cpu_devices_count=1,--lazy_load_tensors=true,--save_dtype=bfloat16,async_checkpointing=falsex86_64(8xH100 node) andaarch64(GB200/GB300 nodes). The failure is platform-independent — it happens before any device work.Additional Context
Reported by NVIDIA. This is a regression relative to the pre-Orbax-v1 behaviour: the same configuration worked before
common/checkpointing.pymoved to the v1 API, because the v0LazyTensorHandlerwas still consulted at save time. It began failing in our nightly runs on the first day that picked up the v1 migration commit, and the last passing run used MaxText from the day before.This issue was drafted with assistance from the
opusAI model.