rml / Model files

Model files

rml/io opens weight files without reading them: one read-only memory map per file, safetensors and GGUF parsers that check every length and offset against it, tensors that view the mapped bytes and keep the mapping alive, and training checkpoints stored as ordinary safetensors.

On this page
  1. Mapping a file
  2. How weights stay mapped
  3. safetensors
  4. Sharded checkpoints
  5. GGUF
  6. A worked example: the hand-built file
  7. The reader
  8. The writer
  9. Training checkpoints
  10. Who uses it
  11. Tests

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).

ItemSignatureBehaviourSource
Openstatic 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
accessorsdata(), 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.

Who holds the mapping A MappedFile, owned through a shared pointer, maps the whole file read-only. A SafetensorsReader and a GgufReader each hold that shared pointer. A Tensor returned by either reader's Get wraps the mapped bytes and holds the same shared pointer as its keep-alive owner, so it outlives the reader. GgufReader::Bytes returns a plain span with no owner, which is how the LLM WeightSource reads GGUF: it keeps the reader itself alive in a shared pointer, and its WeightMatrix views are spans into the mapping. The OS faults pages in when a kernel first reads them. MappedFile mmap(PROT_READ, MAP_PRIVATE), whole file shared_ptr file_ SafetensorsReader one per shard in SafetensorsModel GgufReader metadata parsed eagerly Get(name) → Tensor view FromBuffer(…, owner = file_): outlives the reader Bytes(name) span, no owner llm::WeightSource → WeightMatrix keeps the reader in a shared_ptr Pages are read from disk when a kernel first touches them.
The ownership chain. 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).

ItemSignatureBehaviourSource
WriteSafetensorsStatus(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::Openstatic 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
GetExpected<Tensor>(name) constA 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, ContainsEvery 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 .safetensors file, an index .json, or a directory, where it prefers the index and falls back to model.safetensors (safetensors_model.cc L48–60).
  • The index. weight_map must be an object of strings, and each shard must be a plain file name: a path with a directory part is InvalidArgument, 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 (a std::map), and Get forwards 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:

Byte offsets of the hand-built GGUF test file Bytes 0 to 24 are the header: the magic GGUF, version 3, one tensor and one metadata key. Bytes 24 to 69 are the metadata entry general.architecture of type 8, string, with value llama. Bytes 69 to 110 are the tensor table entry: name t, two dims stored as 3 then 2, ggml type 0 which is F32, and offset 0. Bytes 110 to 128 are zero padding to the 32-byte alignment. Bytes 128 to 152 are the six floats 0 to 5. The reader reverses the dims to the row-major shape 2 by 3, so element [1, 0] is 3. header GGUF, v3, 1 tensor, 1 key metadata general.architecture type 8 = "llama" tensor table "t", dims {3, 2} type 0, offset 0 pad data 6 × f32: 0 1 2 3 4 5 0 24 69 110 128 152 GgufTensorInfo shape [2, 3] (dims reversed), F32 offset 0, bytes 6 / 1 · 4 = 24 data_start_ = 128 AlignUp(110, 32) tensor at 128 + 0 Get("t"): [[0, 1, 2], [3, 4, 5]], a view on the mapping t[1, 0] = 3, t[1, 2] = 5, as the test checks
Offsets are from the test's construction: the key length sits at byte 24 and the tensor count at byte 8, which is where the malformed-file test plants a huge length and a 260 count (gguf_test.cc L196–230).

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):

StepRejectsStatus
HeaderWrong magic (0x46554747, "GGUF" as a little-endian u32); a short header; a version other than 2 or 3InvalidArgument; version is Unsupported
Metadata countMore keys than the remaining bytes could hold at 12 bytes each, before anything is allocatedInvalidArgument ("truncated")
Each keyA 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 keyInvalidArgument (L183–209)
general.alignmentNot an integer, not positive, above 220, or not a power of twoInvalidArgument
Tensor countMore entries than the remaining bytes could hold at 24 bytes eachInvalidArgument
Each tensorRank 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 nameInvalidArgument
DataThe data section starts at the table's end rounded up to the alignment; any tensor that runs past the end of the fileInvalidArgument

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.

AccessorReturnsSource
GetInt, GetFloat, GetStringOne 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, GetStringsAn array of that kind, such as a tokenizer's token list..cc L531–554
tensors, InfoThe table in file order, or one entry by name..h L125–127
BytesThe raw mapped bytes of a tensor of any known type, Q4_K included: what the LLM runtime reads..cc L562–567
GetA 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).

FunctionSignatureBehaviourSource
SaveCheckpointStatus(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
LoadCheckpointExpected<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

CallerReadsHowSource
llm::WeightSourceGGUF or sharded safetensorsLooks 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 tokenizersGGUF metadata, config.json, tokenizer.jsonHyperparameters and vocabularies from GGUF keys, or from Hugging Face JSON files opened through MappedFile.config.cc L370, hf_tokenizer.cc L296
graph::LoadGraph<stem>.safetensorsOpens 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

FileTestsWhat they pin down
mapped_file_test.cc3The mapping holds the file's bytes; an empty file maps to zero bytes; a missing file is NotFound.
safetensors_test.cc8Every 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.cc5A 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.cc5The 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.cc5Training 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.