SPB Git

spb/forge Public MIT

Forge — LLM training from scratch in pure C++20 + Metal on Apple Silicon.

C++ 61.2% C 23% Python 7.6% TeX 7.2% CMake 1.1%
3.2 KB · 81 lines c
Raw Blame History
1// Author: Simon-Pierre Boucher — contact@spboucher.ai2#pragma once34#include "core/autograd.h"5#include "core/tensor.h"67#include <cstdint>8#include <memory>9#include <string>10#include <utility>11#include <vector>1213// .forge — Forge's home-made, Apple-native, git-style weight format.14//15// A .forge is a DIRECTORY (a model repository):16//17//   model.forge/18//     manifest-latest.json              <- HEAD: copy of the newest manifest19//     manifests/manifest-000042.json    <- one "commit" per save(): step, tag,20//                                          parent, config, tensor index21//     objects/<16-hex>.fshard           <- immutable content-addressed shards22//23// Three properties make it worth existing next to .pt/safetensors/gguf:24//25//  1. APPLE-NATIVE ZERO-COPY. Every tensor is aligned to the Apple Silicon26//     16 KB page inside its shard, and shards are padded to page multiples,27//     so loading is mmap + newBuffer(bytesNoCopy): the file-cache pages ARE28//     the GPU memory (MTLStorageModeShared unified memory). No parse, no29//     memcpy — a multi-GB model "loads" in milliseconds.30//31//  2. GIT-STYLE DELTA SAVES. Tensors are content-addressed (FNV-1a 64).32//     save() rewrites only tensors whose bytes changed since the parent33//     manifest; unchanged ones are referenced in place. Manifests are tiny34//     JSON commits with a parent link, so a repo carries its whole history.35//36//  3. GITHUB-FRIENDLY SHARDING. New tensors are packed into shard files37//     capped at shard_mb (default 95 MB — under GitHub's 100 MB limit), so38//     a repo can be pushed as-is.39//40// dtypes: f32 (zero-copy alias at load) · f16 / bf16 (half size on disk,41// converted to f32 at load until the mixed-precision kernels land).42namespace forge::fmodel {4344struct SaveOptions {45    DType dtype = DType::F32; // f32 | f16 | bf16 storage46    int64_t shard_mb = 95;    // shard cap; a bigger single tensor gets its own47    std::string tag;          // optional human label for the manifest48    int64_t step = -1;        // training step recorded in the manifest49};5051// Snapshot the (deduped) parameters into repo_dir, creating it if needed.52// Returns the manifest path. Repeated saves write only changed tensors.53std::string save(const std::string& repo_dir, const std::string& config_json,54                 const std::vector<std::pair<std::string, Var>>& named_params,55                 const SaveOptions& opts);5657// Zero-copy reader for one manifest.58class Snapshot {59public:60    // path: a .forge directory (opens manifest-latest.json) or an explicit61    // manifest .json inside one. Dies with a message on malformed repos.62    static Snapshot open(const std::string& path);6364    const std::string& config_json() const;65    int64_t step() const;66    std::vector<std::string> names() const;67    bool has(const std::string& name) const;68    const std::vector<int64_t>& shape(const std::string& name) const;6970    // f32 tensors alias the mmapped shard (READ-ONLY by contract: they feed71    // forward passes, never optimizer updates). Other dtypes convert into a72    // fresh f32 tensor.73    Tensor tensor_f32(const std::string& name) const;7475private:76    struct Impl;77    std::shared_ptr<Impl> impl_;78};7980} // namespace forge::fmodel81