In short: MappedFile maps a whole file read-only and is shared by everything that views it.
SafetensorsReader parses the 8-byte length and JSON header, and SafetensorsModel
joins the shards of a Hugging Face checkpoint through its index. GgufReader parses llama.cpp's
container (versions 2 and 3), reverses each tensor's dims into rml's row-major order, and hands out raw byte
spans for any ggml type or Tensor views for the few rml has dtypes for. Parsing touches only
headers; tensor bytes are paged in by the OS the first time a kernel reads them. Every header field is bounds
checked, so a truncated or hostile file is an error, never a read past the mapping. Checkpoints write a model's
parameters, buffers and optimizer state as one safetensors file and load it back strictly.
The concepts are on /ml-models: checkpointing and quantization. The module is five libraries, one per header, each with a test (BUILD.bazel). rml's architecture notes state the rule this module implements: weights are never copied into the heap, so resident memory is the pages the OS has faulted in (architecture.md L66–70).
Mapping a file
The header gives the reason in three lines: the OS pages mapped weights in on first touch and can drop clean pages under memory pressure, so a model larger than free RAM still loads, and loading costs nothing until a tensor is used (mapped_file.h L4–8).
| Item | Signature | Behaviour | Source |
|---|---|---|---|
Open | static Expected<shared_ptr<const MappedFile>>(const string& path) | open, fstat, then mmap(PROT_READ, MAP_PRIVATE) of the whole file, and the descriptor is closed at once since the mapping outlives it. A missing file is NotFound, any other failure IoError with strerror. An empty file is a valid empty mapping, because mmap of zero bytes fails. | .h L22–23, .cc L18–44 |
| accessors | data(), size(), bytes(), path() | The mapped bytes as a pointer, a size or a span<const byte>. | .h L29–32 |
| destructor | ~MappedFile() | munmap. Copying is deleted; sharing is through the shared_ptr, so the last holder unmaps. | .h L25–27, .cc L46–48 |
Nothing in rml calls madvise at this commit. rml's memory notes plan layer-by-layer golden runs of
an 8B model at bf16 that drop each layer's pages with madvise(MADV_DONTNEED); that is
planned (models.md L41–43, see
Models and memory).
How weights stay mapped
A tensor that views a mapping must keep it alive, and a reader must be able to go away first. rml does this
with one shared owner. Tensor::FromBuffer(data, shape, dtype, owner) wraps external memory with a
shared_ptr<const void> keep-alive handle (tensor.h
L88–89, Tensors and dtypes), and both readers pass their MappedFile as
that owner.
SafetensorsTest.TensorsViewTheMappingAndOutliveTheReader checks
the left branch (safetensors_test.cc L84–94); the
right one is weight_source.h L65–66.
The mapping is read-only but Tensor's data pointer is not const, so both readers
const_cast it and document that callers must not write through it
(safetensors.cc L191–193,
gguf.cc L577–579). A write would fault, which is why
LoadCheckpoint copies values into the model's own tensors rather than keeping views (below).
safetensors
The format is an 8-byte little-endian header length, a JSON object mapping each tensor name to
{dtype, shape, data_offsets} plus an optional __metadata__ object of string pairs, then
the raw little-endian bytes (safetensors.h L4–11).
| Item | Signature | Behaviour | Source |
|---|---|---|---|
WriteSafetensors | Status(path, const NamedTensors&, const Metadata& = {}) | Writes in the given order, each tensor made contiguous first, so strided views are stored in logical order. The JSON is padded with spaces so the data starts 8-byte aligned, as the reference implementation does. A duplicate name or __metadata__ is InvalidArgument; a dtype outside F32, F16, BF16, I32 and I8 (the block-quantized ones) is Unsupported. | .h L32–34, .cc L69–111 |
SafetensorsReader::Open | static Expected<SafetensorsReader>(path) | Maps the file and parses the header without exceptions. Rejects a file under 8 bytes, a header length over 100 MiB or past the end, a header that is not an object, non-string metadata, a malformed entry, an unknown dtype, a dimension or element count above 240, offsets outside the data, and a byte size that disagrees with dtype and shape. Names are kept sorted by data offset (stable), which is file order. | .cc L28–29, L113–185 |
Get | Expected<Tensor>(name) const | A view onto the mapping. If the bytes are not aligned for the element type, which other writers can produce, it returns a heap copy instead of making unaligned loads. A missing name is NotFound. | .cc L187–202 |
GetAll, names, metadata, Contains | Every tensor in file order; the metadata map; membership. | .h L40–48, .cc L204–212 |
Sharded checkpoints
Large Hugging Face checkpoints split their tensors across files such as
model-00001-of-00004.safetensors, with model.safetensors.index.json mapping each name
to its shard under weight_map. SafetensorsModel maps every shard, so opening an
8B-parameter model costs only the headers (safetensors_model.h
L4–10).
- What it opens. A
.safetensorsfile, an index.json, or a directory, where it prefers the index and falls back tomodel.safetensors(safetensors_model.cc L48–60). - The index.
weight_mapmust be an object of strings, and each shard must be a plain file name: a path with a directory part isInvalidArgument, so an index cannot point outside its own directory (L24–44). - Agreement. Each shard is opened once, the first time the index names it, and every name the index assigns to a shard must actually be in that shard's header (L62–76).
- Lookup.
names()is sorted (astd::map), andGetforwards to the shard's reader (L79–90).
GGUF
GGUF is llama.cpp's single-file container: metadata (architecture, hyperparameters, the tokenizer's vocabulary) and every tensor in one file. The header comment is the format description rml works from (gguf.h L4–21):
"GGUF", u32 version, u64 tensor count, u64 metadata count;- metadata: a string key, a u32 value type and the value, where an array holds an element type, a u64 count and the elements, and a string is a u64 length and its bytes;
- the tensor table: a string name, u32 n_dims, u64 dims, a u32 ggml type and a u64 offset into the data section;
- padding to
general.alignment(default 32), then the data.
GGUF lists dims innermost first, so a PyTorch weight of shape [out, in] is stored as
{in, out}; the reader reverses them into rml's row-major Shape. The format is read in
place as little-endian, which a static_assert on the host's byte order guarantees
(gguf.cc L27–30).
A worked example: the hand-built file
gguf_test builds a file byte by byte from the spec, one string key and one 2 × 3 f32 tensor
(gguf_test.cc L43–63). Its layout:
The reader
GgufReader::Open walks the mapping with a small Cursor that copies each field out with
memcpy and refuses to run past the end (gguf.cc
L101–128). The checks, in order (L413–499):
| Step | Rejects | Status |
|---|---|---|
| Header | Wrong magic (0x46554747, "GGUF" as a little-endian u32); a short header; a version other than 2 or 3 | InvalidArgument; version is Unsupported |
| Metadata count | More keys than the remaining bytes could hold at 12 bytes each, before anything is allocated | InvalidArgument ("truncated") |
| Each key | A truncated key or value; a value type above 12; an array of arrays (Unsupported); an array count larger than the remaining bytes allow at the element's minimum size (8 for strings); a duplicate key | InvalidArgument (L183–209) |
general.alignment | Not an integer, not positive, above 220, or not a power of two | InvalidArgument |
| Tensor count | More entries than the remaining bytes could hold at 24 bytes each | InvalidArgument |
| Each tensor | Rank 0 or above 4; a zero dim, a dim above 240 or more than 248 elements; an unknown ggml type (Unsupported); rows that are not whole blocks; an offset not a multiple of the alignment; a duplicate name | InvalidArgument |
| Data | The data section starts at the table's end rounded up to the alignment; any tensor that runs past the end of the file | InvalidArgument |
Metadata values are parsed into GgufValue: integers and bools in ints, floats in
floats, strings in strings, with the element type kept for arrays
(gguf.h L42–83). Tensors are only recorded as
GgufTensorInfo (row-major shape, type, offset, byte size); their bytes are not read.
| Accessor | Returns | Source |
|---|---|---|
GetInt, GetFloat, GetString | One scalar. NotFound for a missing key, InvalidArgument for the wrong kind; GetInt accepts any integer type or bool, GetFloat F32 or F64. | .h L116–120, .cc L507–529 |
GetInts, GetFloats, GetStrings | An array of that kind, such as a tokenizer's token list. | .cc L531–554 |
tensors, Info | The table in file order, or one entry by name. | .h L125–127 |
Bytes | The raw mapped bytes of a tensor of any known type, Q4_K included: what the LLM runtime reads. | .cc L562–567 |
Get | A read-only Tensor view for the types rml has a dtype for (F32, F16, BF16, I8, I32, Q8_0, Q4_0); Unsupported for the K-quants and the rest. | .cc L32–51, L569–580 |
The writer
rml also writes GGUF: the llm tests and the decoder benchmark build their files with it. The raw form takes
GgufTensorData (name, shape, ggml type, bytes) and an alignment
(gguf.cc L326–396): always version 3; a power-of-two
alignment, written as a leading general.alignment key only when it is not 32 (passing that key
yourself is an error); each tensor checked for whole-block rows, rank 1 to 4 and the exact byte count; dims
written innermost first; and every tensor's offset rounded up to the alignment, with the header and gaps
zero-padded. The Tensor overload makes each tensor contiguous and maps its dtype to a ggml type
(L398–411).
Training checkpoints
A checkpoint is one safetensors file holding a model's parameters and buffers, an optimizer's state and
free-form metadata, so training can stop and resume exactly. Keys are model. plus the dotted
parameter or buffer path and optim. plus the optimizer's state name, and any safetensors tool can
read the file (checkpoint.h L4–11).
| Function | Signature | Behaviour | Source |
|---|---|---|---|
SaveCheckpoint | Status(path, const nn::Module&, const optim::Optimizer* = nullptr, const Metadata& = {}) | Collects NamedParameters() and NamedBuffers(), then the optimizer's NamedState() when one is given, and writes them with WriteSafetensors. | .cc L21–43 |
LoadCheckpoint | Expected<Metadata>(path, nn::Module&, optim::Optimizer* = nullptr) | Strict: every model tensor must be present with the same shape and dtype (NotFound or InvalidArgument), and an unknown model. key or a key with neither prefix is InvalidArgument. Values are copied into the model's existing tensors with CopyFrom. With an optimizer, its state is required and restored through LoadState. On error the model may be partly loaded. Returns the saved metadata. | .h L26–32, .cc L45–81 |
The state each optimizer saves is on Losses and optimizers, and the modules whose parameters are named here on Neural network layers. No example or other module calls the checkpoint functions at this commit; their test is the user.
Who uses it
| Caller | Reads | How | Source |
|---|---|---|---|
llm::WeightSource | GGUF or sharded safetensors | Looks tensors up by GGUF name, mapping Hugging Face names to them, and builds WeightMatrix views over the mapped bytes; only norm weights and biases are copied out as f32. For GGUF it takes Bytes directly; for safetensors it takes a tensor's raw_data(). | weight_source.h L4–11, .cc L134–149 |
llm config and tokenizers | GGUF metadata, config.json, tokenizer.json | Hyperparameters and vocabularies from GGUF keys, or from Hugging Face JSON files opened through MappedFile. | config.cc L370, hf_tokenizer.cc L296 |
graph::LoadGraph | <stem>.safetensors | Opens the weights next to an rml-graph JSON file, if present, and takes each constant as a view. | graph.cc L208–224, The rml-graph IR |
One edge in WeightSource::Raw: when a safetensors tensor is misaligned, Get returns a heap
copy, and Raw keeps only a span of that copy's bytes while the copy itself goes out of scope at the
end of the function, so the span would outlive its storage. Aligned files, which is what rml's writer and the
usual Hugging Face files produce, take the view path and are unaffected
(weight_source.cc L143–148). How the decoder uses
these views is on The decoder, the GGUF tokenizer on
Tokenizers, and the block formats behind Bytes on
Quantization.
Tests
| File | Tests | What they pin down |
|---|---|---|
mapped_file_test.cc | 3 | The mapping holds the file's bytes; an empty file maps to zero bytes; a missing file is NotFound. |
safetensors_test.cc | 8 | Every dtype round-trips; strided views are written in logical order; tensors outlive the reader; GetAll is file order; duplicate, reserved and quantized writes fail; a missing name is NotFound; eleven malformed files (bad JSON, an array header, missing offsets, F64, past the end, size mismatch, a negative or a huge shape, non-string metadata, too short, header length past the end) are InvalidArgument; a misaligned F32 tensor is copied and read correctly. |
safetensors_model_test.cc | 5 | A sharded directory, the index file directly, a single file or its directory all open; an index naming a tensor its shard lacks, a shard path with ../, an index that is not JSON, and a missing shard are errors. |
gguf_test.cc | 5 | The hand-built file above; every metadata type and array kind round-trips with f32, transposed-view, f16 and Q8_0 tensors, each at an aligned offset; a raw Q4_K tensor at alignment 64 reads back byte for byte and Get refuses it, while alignment 48 and half-block rows are refused on write; every proper prefix of the file fails to open; bad magic, version 9, a misaligned offset, type 4, a huge string length and a 260 tensor count are rejected. |
checkpoint_test.cc | 5 | Training 3 Adam steps, saving, loading into a model built from another seed and training 3 more matches 6 uninterrupted steps; a loaded parameter can be written (no aliasing of the read-only mapping); keys carry the two prefixes; mismatched models are rejected; optimizer state is required when an optimizer is passed. |