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%
1// Author: Simon-Pierre Boucher — contact@spboucher.ai2#include "core/tensor.h"34#include "core/device.h"56#include <Metal/Metal.hpp>78#include <cstdio>9#include <cstdlib>10#include <cstring>11#include <sstream>1213namespace forge {1415namespace {16[[noreturn]] void die(const std::string& msg) {17 std::fprintf(stderr, "forge: %s\n", msg.c_str());18 std::abort();19}20} // namespace2122// Owns one pooled MTLBuffer; returns it to the pool when the last Tensor23// view drops it. External storages (fmodel mmaps) retain a caller-owned24// buffer instead and release it directly — the allocator never sees it.25struct Tensor::Storage {26 MTL::Buffer* buffer = nullptr;27 bool external = false;2829 explicit Storage(size_t nbytes) {30 buffer = Device::get().allocator().acquire(nbytes);31 }32 Storage(MTL::Buffer* ext, bool) : buffer(ext), external(true) {33 buffer->retain();34 }35 ~Storage() {36 if (external) buffer->release();37 else Device::get().allocator().release(buffer);38 }39 Storage(const Storage&) = delete;40 Storage& operator=(const Storage&) = delete;41};4243std::vector<int64_t> Tensor::contiguous_strides(const std::vector<int64_t>& shape) {44 std::vector<int64_t> strides(shape.size());45 int64_t acc = 1;46 for (int64_t i = int64_t(shape.size()) - 1; i >= 0; --i) {47 strides[size_t(i)] = acc;48 acc *= shape[size_t(i)];49 }50 return strides;51}5253Tensor Tensor::empty(std::vector<int64_t> shape, DType dtype) {54 Tensor t;55 t.dtype_ = dtype;56 t.shape_ = std::move(shape);57 t.strides_ = contiguous_strides(t.shape_);58 int64_t n = 1;59 for (int64_t d : t.shape_) {60 if (d <= 0) die("Tensor::empty: non-positive dim");61 n *= d;62 }63 t.storage_ = std::make_shared<Storage>(size_t(n) * dtype_size(dtype));64 return t;65}6667Tensor Tensor::from_buffer(MTL::Buffer* buffer, size_t byte_offset,68 std::vector<int64_t> shape, DType dtype) {69 Tensor t;70 t.dtype_ = dtype;71 t.shape_ = std::move(shape);72 t.strides_ = contiguous_strides(t.shape_);73 if (byte_offset % dtype_size(dtype) != 0)74 die("Tensor::from_buffer: offset not aligned to dtype size");75 t.offset_ = int64_t(byte_offset / dtype_size(dtype));76 t.storage_ = std::make_shared<Storage>(buffer, true);77 return t;78}7980Tensor Tensor::zeros(std::vector<int64_t> shape, DType dtype) {81 Tensor t = empty(std::move(shape), dtype);82 std::memset(t.raw(), 0, t.nbytes());83 return t;84}8586Tensor Tensor::full(std::vector<int64_t> shape, float value, DType dtype) {87 Tensor t = empty(std::move(shape), dtype);88 t.fill_(value);89 return t;90}9192int64_t Tensor::size(int64_t dim) const {93 if (dim < 0) dim += ndim();94 if (dim < 0 || dim >= ndim()) die("Tensor::size: dim out of range");95 return shape_[size_t(dim)];96}9798int64_t Tensor::numel() const {99 int64_t n = 1;100 for (int64_t d : shape_) n *= d;101 return n;102}103104bool Tensor::is_contiguous() const {105 return strides_ == contiguous_strides(shape_);106}107108void* Tensor::raw() const {109 if (!storage_) die("Tensor::raw: undefined tensor");110 return static_cast<char*>(storage_->buffer->contents()) + size_t(offset_) * itemsize();111}112113MTL::Buffer* Tensor::buffer() const {114 if (!storage_) die("Tensor::buffer: undefined tensor");115 return storage_->buffer;116}117118size_t Tensor::buffer_offset() const {119 return size_t(offset_) * itemsize();120}121122Tensor Tensor::view(std::vector<int64_t> new_shape) const {123 if (!is_contiguous()) die("Tensor::view: tensor not contiguous");124 int64_t n = 1;125 for (int64_t d : new_shape) n *= d;126 if (n != numel()) die("Tensor::view: numel mismatch");127 Tensor t = *this;128 t.shape_ = std::move(new_shape);129 t.strides_ = contiguous_strides(t.shape_);130 return t;131}132133Tensor Tensor::slice0(int64_t start, int64_t len) const {134 if (!is_contiguous()) die("Tensor::slice0: tensor not contiguous");135 if (ndim() < 1 || start < 0 || len <= 0 || start + len > shape_[0])136 die("Tensor::slice0: range out of bounds");137 Tensor t = *this;138 t.shape_[0] = len;139 t.offset_ = offset_ + start * strides_[0];140 return t;141}142143void Tensor::fill_(float value) {144 const int64_t n = numel();145 switch (dtype_) {146 case DType::F32: {147 float* p = data<float>();148 for (int64_t i = 0; i < n; ++i) p[i] = value;149 break;150 }151 case DType::F16: {152 f16_t* p = data<f16_t>();153 const f16_t v = f16_t(value);154 for (int64_t i = 0; i < n; ++i) p[i] = v;155 break;156 }157 case DType::BF16: {158 uint16_t* p = data<uint16_t>();159 const uint16_t v = float_to_bf16(value);160 for (int64_t i = 0; i < n; ++i) p[i] = v;161 break;162 }163 case DType::U16: {164 uint16_t* p = data<uint16_t>();165 const uint16_t v = uint16_t(value);166 for (int64_t i = 0; i < n; ++i) p[i] = v;167 break;168 }169 case DType::I32: {170 int32_t* p = data<int32_t>();171 const int32_t v = int32_t(value);172 for (int64_t i = 0; i < n; ++i) p[i] = v;173 break;174 }175 }176}177178float Tensor::item_at(int64_t i) const {179 switch (dtype_) {180 case DType::F32: return data<float>()[i];181 case DType::F16: return float(data<f16_t>()[i]);182 case DType::BF16: return bf16_to_float(data<uint16_t>()[i]);183 case DType::U16: return float(data<uint16_t>()[i]);184 case DType::I32: return float(data<int32_t>()[i]);185 }186 return 0.0f;187}188189void Tensor::set_item(int64_t i, float value) {190 switch (dtype_) {191 case DType::F32: data<float>()[i] = value; break;192 case DType::F16: data<f16_t>()[i] = f16_t(value); break;193 case DType::BF16: data<uint16_t>()[i] = float_to_bf16(value); break;194 case DType::U16: data<uint16_t>()[i] = uint16_t(value); break;195 case DType::I32: data<int32_t>()[i] = int32_t(value); break;196 }197}198199std::string Tensor::describe() const {200 std::ostringstream os;201 os << "Tensor(" << dtype_name(dtype_) << ", [";202 for (size_t i = 0; i < shape_.size(); ++i) {203 if (i) os << ", ";204 os << shape_[i];205 }206 os << "])";207 return os.str();208}209210} // namespace forge211