DEV Community

kai wen ng
kai wen ng

Posted on

Loading Safetensors in C++

First, use open() to open the file and check the file size.

fd = open(path.c_str(), O_RDONLY);
file_size = lseek(fd, 0, SEEK_END);
Enter fullscreen mode Exit fullscreen mode

To avoid loading the entire file into memory, I use mmap() to map the file into virtual memory.

mapped = mmap(nullptr, file_size, PROT_READ, MAP_PRIVATE, fd, 0);
Enter fullscreen mode Exit fullscreen mode

The first uint64_t in a safetensors file stores the header size.

uint64_t header_size;
memcpy(&header_size, mapped, sizeof(uint64_t));

char *header_ptr = static_cast<char *>(mapped) + sizeof(uint64_t);

std::string header(header_ptr, header_size);
Enter fullscreen mode Exit fullscreen mode

The header contains the metadata information of each tensor. To parse the metadata, I use nlohmann::json.

metadata = nlohmann::json::parse(header);
Enter fullscreen mode Exit fullscreen mode

The actual model parameters start after the header:

data_offset = sizeof(uint64_t) + header_size;
Enter fullscreen mode Exit fullscreen mode

To access each layer's weights easily, I created a Tensor structure:

struct Tensor
{
    std::string name;
    std::string dtype;

    std::vector<int64_t> shape;

    uint64_t start;
    uint64_t end;

    char *data;
    size_t size;
};
Enter fullscreen mode Exit fullscreen mode

A get_tensor() function is used to retrieve each tensor from the safetensors file. The important part is calculating the correct memory position using the tensor's data offsets.

Tensor get_tensor(const std::string &name)
{
    if (!metadata.contains(name))
        throw std::runtime_error("Tensor not found: " + name);

    auto json = metadata[name];

    Tensor tensor;

    tensor.name = name;
    tensor.dtype = json["dtype"];
    tensor.shape = json["shape"].get<std::vector<int64_t>>();

    tensor.start = json["data_offsets"][0].get<uint64_t>();
    tensor.end = json["data_offsets"][1].get<uint64_t>();

    tensor.size = tensor.end - tensor.start;

    tensor.data =
        static_cast<char *>(mapped) + data_offset + tensor.start;

    return tensor;
}
Enter fullscreen mode Exit fullscreen mode

Since my PC has limited memory, I avoid loading the entire model weights. Instead, the weights remain inside the memory-mapped file, and the values are converted only when needed during inference. Note that this does not copy the actual parameters into memory; it only creates a pointer to the corresponding location inside the memory-mapped file.
During inference, the tensor data can be accessed by casting the raw memory pointer into the correct data type.

For BF16 weights:

embedding_weights = reinterpret_cast<uint16_t *>(tensor.data);
output.resize(embedding_dim);
size_t offset = token_id * embedding_dim;
for (int i = 0; i < embedding_dim; i++)
{
    output[i] = bf16_to_float(embedding_weights[offset + i]);
}
Enter fullscreen mode Exit fullscreen mode

Alternatively, for faster inference, the tensor can be copied into allocated memory instead of accessing the memory-mapped file directly.

tensor.data = static_cast<char *>(malloc(tensor.size));
memcpy(
    tensor.data,
    static_cast<char *>(mapped) + data_offset + start,
    tensor.size
);

uint16_t *weights = reinterpret_cast<uint16_t *>(tensor.data);
for (int i = 0; i < size; i++)
{
    float value = bf16_to_float(weights[i]);
}
Enter fullscreen mode Exit fullscreen mode

This loads the tensor data into normal allocated memory, allowing direct access during inference.

Top comments (0)