HuggingFace Hub integration¶
NNx ships first-class interop with the HuggingFace ecosystem:
- safetensors as an opt-in checkpoint format on
NNCheckpoint— safe (no arbitrary-code unpickling), mmap-friendly, and readable by ComfyUI / vLLM / AutoGPTQ /transformerstools. PyTorchModelHubMixinonNNModel— freesave_pretrained/push_to_hub/from_pretrainedfor distributing models via the Hub.
Both paths require the hub extra:
pip install "thekaveh-nnx[hub]"
Without it, the rest of NNx keeps working — the integration is gated
behind import-time guards. Calling any Hub method without the extra
raises a clear ImportError pointing back at this install line.
1. safetensors checkpoints¶
1.1. When to use it¶
Use safetensors when any of the following is true:
- You plan to publish weights to the Hub or share them with anyone
outside your machine. Pickle checkpoints can execute arbitrary code
on load (see the security note on
NNCheckpoint.from_file); safetensors cannot. - You need to load weights into a non-Python tool (ComfyUI, vLLM,
AutoGPTQ,
transformers-aware loaders). - You care about mmap-based zero-copy loads — safetensors files are laid out so the underlying tensors can be mapped directly from disk.
Pickle remains the default and is the right choice for local-only
training runs where the convenience of torch.save-ing the full
dataclass (preserving the OrderedDict key order and the
NNCheckpoint identity) outweighs the security trade-off.
1.2. Writing a safetensors checkpoint¶
NNCheckpoint.to_file takes a format kwarg:
from nnx import NNCheckpoint
# Build a checkpoint as usual…
ckpt = NNCheckpoint(
idp=..., # NNIterationDataPoint
model_params=model.params,
net_params=model.net_params,
net_state=model.net.state_dict(),
)
# …then write either format. Pickle is the default.
ckpt.to_file("checkpoint.pt") # legacy default
ckpt.to_file("checkpoint.safetensors", format="safetensors")
NNParams, NNModelParams, and NNIterationDataPoint are
JSON-serialized into the safetensors metadata dict (the format spec
limits metadata to str -> str, so a JSON wrapper is the cleanest
fit). The net's tensors are detached and made contiguous, then written
through safetensors' standard save_file.
Writes are atomic — the file is staged at <path>.tmp and os.replace-d
into place — matching the same KeyboardInterrupt-safety guarantee that
the pickle path provides.
1.3. Reading a checkpoint of either format¶
NNCheckpoint.from_file auto-detects which format the file was written
in by sniffing the first few bytes:
- Modern
torch.saveproduces a ZIP container that starts withb"PK\x03\x04". - Legacy
torch.save(with_use_new_zipfile_serialization=False) and bare pickle files start with the\x80PROTO opcode. - safetensors files start with a little-endian u64 header length
followed by a JSON object — byte 8 is always
{. (The u64's low byte can legitimately be0x80, colliding with the pickle PROTO opcode, so the loader positively identifies safetensors via byte 8 before the pickle sniff.)
The same call works for both:
ckpt = NNCheckpoint.from_file("checkpoint.safetensors") # or .pt
model = NNModel.from_checkpoint(ckpt)
2. Publishing an NNModel to the Hub¶
2.1. When to use the Hub mixin¶
Use save_pretrained / push_to_hub / from_pretrained for
distribution: shipping a trained NNModel so others can
from_pretrained("you/your-model") and run it. The flat on-disk layout
this writes (model.safetensors + config.json + README.md) is what
the Hub expects, and it's what downstream tools probe for.
Keep using NNCheckpoint for local training state: the
runs/<id>/checkpoints/ layout that NNx writes during training carries
per-epoch IDPs, optimizer state sidecars, and run.id-keyed metadata
that the Hub layout deliberately strips.
2.2. Save a model locally¶
from nnx import NNModel, NNParams, NNModelParams, Activations, Devices, Losses, Nets
model = NNModel(
net_params=NNParams(
input_dim=4, output_dim=2, hidden_dims=[8],
dropout_prob=0.0, activation=Activations.RELU,
),
params=NNModelParams(
net=Nets.FEED_FWD, device=Devices.CPU, loss=Losses.CROSS_ENTROPY,
),
)
# …train…
model.save_pretrained("./my-model")
This writes three files into ./my-model/:
model.safetensors—self.net.state_dict()as safetensors.config.json—{"net_params": <state>, "params": <state>}, using the same publicstate()form NNRun hashes forrun.idgrouping.README.md— auto-generated model card from the mixin.
2.3. Load from a local directory¶
from nnx import NNModel
model = NNModel.from_pretrained("./my-model")
from_pretrained reads config.json, rebuilds NNParams and
NNModelParams via their public from_state constructors, then loads
the safetensors weights into the freshly-built self.net. Bit-exact
round-trip on tensors; state() form identical on the params.
2.4. Publish to the Hub¶
# One-time login (writes a token to ~/.cache/huggingface/token):
# hf auth login
model.push_to_hub("your-user/your-model")
The mixin handles repo creation, file upload, and commit. Everything
that save_pretrained writes locally is pushed.
2.5. Load from the Hub¶
model = NNModel.from_pretrained("your-user/your-model")
Hugging Face's cache directory is used transparently — repeat loads hit the local cache, not the network.
3. What this does NOT do¶
NNRunis not Hub-published. The Hub layout is per-model, not per-training-run. If you want to publish a full training run (idps.csv + run.yaml + every per-phase checkpoint), upload theruns/<id>/directory directly viahuggingface_hub.upload_folder.- Optimizer state is not in the Hub config.
save_pretrainedwrites only the network weights; resuming optimizer state from a Hub-loaded model isn't supported. UseNNCheckpointfor warm-resume workflows. - The Hub mixin doesn't rewrite
NNModel's constructor. It still takes(net_params, params)keyword args at__init__— the mixin is purely additive. Existing code keeps working unchanged.