src/safetensors.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "brotensor/safetensors.h" | ||
| 2 | |||
| 3 | #include "brotensor/ops/elementwise.h" | ||
| 4 | #include "brotensor/runtime.h" | ||
| 5 | #include "brotensor/tensor.h" | ||
| 6 | |||
| 7 | #include <cstdint> | ||
| 8 | #include <cstdio> | ||
| 9 | #include <cstring> | ||
| 10 | #include <stdexcept> | ||
| 11 | #include <string> | ||
| 12 | #include <unordered_set> | ||
| 13 | #include <utility> | ||
| 14 | #include <vector> | ||
| 15 | |||
| 16 | #ifdef _WIN32 | ||
| 17 | #define WIN32_LEAN_AND_MEAN | ||
| 18 | #define NOMINMAX | ||
| 19 | #include <windows.h> | ||
| 20 | #else | ||
| 21 | #include <fcntl.h> | ||
| 22 | #include <sys/mman.h> | ||
| 23 | #include <sys/stat.h> | ||
| 24 | #include <unistd.h> | ||
| 25 | #endif | ||
| 26 | |||
| 27 | namespace brotensor::safetensors { | ||
| 28 | |||
| 29 | // ─── Dtype helpers ───────────────────────────────────────────────────────── | ||
| 30 | |||
| 31 | 13 | const char* dtype_name(Dtype d) { | |
| 32 |
9/9✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 2 times.
✓ Branch 6 taken 1 time.
✓ Branch 7 taken 1 time.
✓ Branch 8 taken 1 time.
|
13 | switch (d) { |
| 33 | 1 | case Dtype::F32: return "F32"; | |
| 34 | 1 | case Dtype::F16: return "F16"; | |
| 35 | 2 | case Dtype::BF16: return "BF16"; | |
| 36 | 2 | case Dtype::I32: return "I32"; | |
| 37 | 2 | case Dtype::I64: return "I64"; | |
| 38 | 2 | case Dtype::U8: return "U8"; | |
| 39 | 1 | case Dtype::BOOL: return "BOOL"; | |
| 40 | 1 | case Dtype::Unknown: | |
| 41 | 2 | default: return "Unknown"; | |
| 42 | } | ||
| 43 | 13 | } | |
| 44 | |||
| 45 | 90 | int dtype_size_bytes(Dtype d) { | |
| 46 |
8/8✓ Branch 0 taken 12 times.
✓ Branch 1 taken 15 times.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 4 times.
✓ Branch 4 taken 29 times.
✓ Branch 5 taken 3 times.
✓ Branch 6 taken 20 times.
✓ Branch 7 taken 3 times.
|
90 | switch (d) { |
| 47 | 29 | case Dtype::F32: return 4; | |
| 48 | 12 | case Dtype::F16: return 2; | |
| 49 | 15 | case Dtype::BF16: return 2; | |
| 50 | 4 | case Dtype::I32: return 4; | |
| 51 | 3 | case Dtype::I64: return 8; | |
| 52 | 20 | case Dtype::U8: return 1; | |
| 53 | 3 | case Dtype::BOOL: return 1; | |
| 54 | 4 | default: return 0; | |
| 55 | } | ||
| 56 | 90 | } | |
| 57 | |||
| 58 | 32 | static Dtype parse_dtype(std::string_view s) { | |
| 59 |
2/2✓ Branch 0 taken 14 times.
✓ Branch 1 taken 18 times.
|
32 | if (s == "F32") return Dtype::F32; |
| 60 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 16 times.
|
18 | if (s == "F16") return Dtype::F16; |
| 61 |
2/2✓ Branch 0 taken 3 times.
✓ Branch 1 taken 13 times.
|
16 | if (s == "BF16") return Dtype::BF16; |
| 62 |
2/2✓ Branch 0 taken 1 time.
✓ Branch 1 taken 12 times.
|
13 | if (s == "I32") return Dtype::I32; |
| 63 |
2/2✓ Branch 0 taken 1 time.
✓ Branch 1 taken 11 times.
|
12 | if (s == "I64") return Dtype::I64; |
| 64 |
2/2✓ Branch 0 taken 9 times.
✓ Branch 1 taken 2 times.
|
11 | if (s == "U8") return Dtype::U8; |
| 65 |
2/2✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1 time.
|
2 | if (s == "BOOL") return Dtype::BOOL; |
| 66 | 1 | return Dtype::Unknown; | |
| 67 | 32 | } | |
| 68 | |||
| 69 | 43 | int64_t TensorView::numel() const { | |
| 70 | 43 | int64_t n = 1; | |
| 71 |
2/2✓ Branch 0 taken 54 times.
✓ Branch 1 taken 40 times.
|
94 | for (int64_t d : shape) { |
| 72 |
2/2✓ Branch 0 taken 52 times.
✓ Branch 1 taken 2 times.
|
54 | if (d < 0) |
| 73 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | throw std::runtime_error("safetensors: negative dimension in tensor shape"); |
| 74 |
4/4✓ Branch 0 taken 49 times.
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 48 times.
✓ Branch 3 taken 1 time.
|
52 | if (d != 0 && n > INT64_MAX / d) |
| 75 |
1/2✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
|
1 | throw std::runtime_error("safetensors: tensor element count overflows int64"); |
| 76 | 51 | n *= d; | |
| 77 | } | ||
| 78 | 40 | return n; | |
| 79 | ✗ | } | |
| 80 | |||
| 81 | // ─── Minimal JSON parser ─────────────────────────────────────────────────── | ||
| 82 | // | ||
| 83 | // Safetensors headers use a strict subset of JSON: top-level object whose | ||
| 84 | // values are either an object {dtype, shape, data_offsets} or the | ||
| 85 | // __metadata__ string->string object. We parse exactly that and reject | ||
| 86 | // anything that doesn't match. No nested escapes beyond \\, \", \n, \t are | ||
| 87 | // expected in tensor names; we handle them defensively anyway. | ||
| 88 | |||
| 89 | namespace { | ||
| 90 | |||
| 91 | struct Parser { | ||
| 92 | const char* p; | ||
| 93 | const char* end; | ||
| 94 | |||
| 95 | 8 | [[noreturn]] void fail(const std::string& msg) const { | |
| 96 |
4/8✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 8 times.
✓ Branch 6 taken 8 times.
✗ Branch 7 not taken.
|
8 | throw std::runtime_error("safetensors: parse error: " + msg); |
| 97 | 8 | } | |
| 98 | |||
| 99 | 1145 | void skip_ws() { | |
| 100 |
9/10✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1164 times.
✓ Branch 2 taken 1145 times.
✓ Branch 3 taken 19 times.
✓ Branch 4 taken 1145 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 time.
✓ Branch 7 taken 1144 times.
✓ Branch 8 taken 20 times.
✓ Branch 9 taken 1145 times.
|
2310 | while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) ++p; |
| 101 | 1145 | } | |
| 102 | |||
| 103 | 629 | bool eat(char c) { | |
| 104 | 629 | skip_ws(); | |
| 105 |
4/4✓ Branch 0 taken 628 times.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 172 times.
✓ Branch 3 taken 456 times.
|
629 | if (p < end && *p == c) { ++p; return true; } |
| 106 | 173 | return false; | |
| 107 | 629 | } | |
| 108 | 392 | void expect(char c) { | |
| 109 |
5/8✓ Branch 0 taken 389 times.
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 3 times.
|
392 | if (!eat(c)) fail(std::string("expected '") + c + "'"); |
| 110 | 392 | } | |
| 111 | |||
| 112 | // Four hex digits at q -> value, or -1 if any is not a hex digit. | ||
| 113 | ✗ | static int hex4(const char* q) { | |
| 114 | ✗ | int v = 0; | |
| 115 | ✗ | for (int i = 0; i < 4; ++i) { | |
| 116 | ✗ | const char c = q[i]; | |
| 117 | int d; | ||
| 118 | ✗ | if (c >= '0' && c <= '9') d = c - '0'; | |
| 119 | ✗ | else if (c >= 'a' && c <= 'f') d = c - 'a' + 10; | |
| 120 | ✗ | else if (c >= 'A' && c <= 'F') d = c - 'A' + 10; | |
| 121 | ✗ | else return -1; | |
| 122 | ✗ | v = (v << 4) | d; | |
| 123 | ✗ | } | |
| 124 | ✗ | return v; | |
| 125 | ✗ | } | |
| 126 | |||
| 127 | ✗ | static void append_utf8(std::string& out, uint32_t cp) { | |
| 128 | ✗ | if (cp < 0x80) { | |
| 129 | ✗ | out += static_cast<char>(cp); | |
| 130 | ✗ | } else if (cp < 0x800) { | |
| 131 | ✗ | out += static_cast<char>(0xC0 | (cp >> 6)); | |
| 132 | ✗ | out += static_cast<char>(0x80 | (cp & 0x3F)); | |
| 133 | ✗ | } else if (cp < 0x10000) { | |
| 134 | ✗ | out += static_cast<char>(0xE0 | (cp >> 12)); | |
| 135 | ✗ | out += static_cast<char>(0x80 | ((cp >> 6) & 0x3F)); | |
| 136 | ✗ | out += static_cast<char>(0x80 | (cp & 0x3F)); | |
| 137 | ✗ | } else { | |
| 138 | ✗ | out += static_cast<char>(0xF0 | (cp >> 18)); | |
| 139 | ✗ | out += static_cast<char>(0x80 | ((cp >> 12) & 0x3F)); | |
| 140 | ✗ | out += static_cast<char>(0x80 | ((cp >> 6) & 0x3F)); | |
| 141 | ✗ | out += static_cast<char>(0x80 | (cp & 0x3F)); | |
| 142 | } | ||
| 143 | ✗ | } | |
| 144 | |||
| 145 | 180 | std::string parse_string() { | |
| 146 | 180 | skip_ws(); | |
| 147 |
3/4✓ Branch 0 taken 179 times.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
|
182 | if (p >= end || *p != '"') fail("expected string"); |
| 148 | 179 | ++p; | |
| 149 | 179 | std::string out; | |
| 150 |
4/4✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1169 times.
✓ Branch 2 taken 992 times.
✓ Branch 3 taken 178 times.
|
1170 | while (p < end && *p != '"') { |
| 151 |
3/4✓ Branch 0 taken 17 times.
✓ Branch 1 taken 975 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 17 times.
|
992 | if (*p == '\\' && p + 1 < end) { |
| 152 | 17 | char e = p[1]; | |
| 153 | // \uXXXX carries a UTF-16 code unit, possibly the high half of | ||
| 154 | // a surrogate pair. It must be decoded and re-emitted as UTF-8: | ||
| 155 | // safetensors headers are UTF-8 JSON, \u is what most writers | ||
| 156 | // emit for a non-ASCII tensor name, and it is what our own | ||
| 157 | // json_escape_append emits for control characters — so without | ||
| 158 | // this we reject both third-party files and our own output. | ||
| 159 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 17 times.
|
17 | if (e == 'u') { |
| 160 | ✗ | if (p + 6 > end) fail("truncated \\u escape in string"); | |
| 161 | ✗ | int hi = hex4(p + 2); | |
| 162 | ✗ | if (hi < 0) fail("bad hex digit in \\u escape"); | |
| 163 | ✗ | p += 6; | |
| 164 | ✗ | uint32_t cp = static_cast<uint32_t>(hi); | |
| 165 | ✗ | if (hi >= 0xD800 && hi <= 0xDBFF) { | |
| 166 | ✗ | if (p + 6 > end || p[0] != '\\' || p[1] != 'u') | |
| 167 | ✗ | fail("unpaired high surrogate in string"); | |
| 168 | ✗ | const int lo = hex4(p + 2); | |
| 169 | ✗ | if (lo < 0) fail("bad hex digit in \\u escape"); | |
| 170 | ✗ | if (lo < 0xDC00 || lo > 0xDFFF) | |
| 171 | ✗ | fail("unpaired high surrogate in string"); | |
| 172 | ✗ | cp = 0x10000u + ((static_cast<uint32_t>(hi) - 0xD800u) << 10) | |
| 173 | ✗ | + (static_cast<uint32_t>(lo) - 0xDC00u); | |
| 174 | ✗ | p += 6; | |
| 175 | ✗ | } else if (hi >= 0xDC00 && hi <= 0xDFFF) { | |
| 176 | ✗ | fail("unpaired low surrogate in string"); | |
| 177 | } | ||
| 178 | ✗ | append_utf8(out, cp); | |
| 179 | ✗ | continue; // p is already past the escape | |
| 180 | } | ||
| 181 |
9/9✓ Branch 0 taken 3 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 1 time.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 2 times.
✓ Branch 6 taken 2 times.
✓ Branch 7 taken 2 times.
✓ Branch 8 taken 1 time.
|
17 | switch (e) { |
| 182 |
1/2✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
|
3 | case '"': out += '"'; break; |
| 183 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | case '\\': out += '\\'; break; |
| 184 |
1/2✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
|
1 | case '/': out += '/'; break; |
| 185 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | case 'n': out += '\n'; break; |
| 186 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | case 't': out += '\t'; break; |
| 187 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | case 'r': out += '\r'; break; |
| 188 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | case 'b': out += '\b'; break; |
| 189 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | case 'f': out += '\f'; break; |
| 190 |
2/4✗ Branch 0 not taken.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
|
1 | default: fail("unsupported escape in string"); |
| 191 | } | ||
| 192 | 16 | p += 2; | |
| 193 | 16 | } else { | |
| 194 |
1/2✓ Branch 0 taken 975 times.
✗ Branch 1 not taken.
|
975 | out += *p++; |
| 195 | } | ||
| 196 | } | ||
| 197 |
4/6✓ Branch 0 taken 1 time.
✓ Branch 1 taken 177 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 time.
✓ Branch 4 taken 1 time.
✗ Branch 5 not taken.
|
178 | if (p >= end) fail("unterminated string"); |
| 198 | 177 | ++p; | |
| 199 | 177 | return out; | |
| 200 | 180 | } | |
| 201 | |||
| 202 | 91 | int64_t parse_int() { | |
| 203 | 91 | skip_ws(); | |
| 204 | 91 | bool neg = false; | |
| 205 |
3/4✓ Branch 0 taken 91 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 90 times.
✓ Branch 3 taken 1 time.
|
91 | if (p < end && *p == '-') { neg = true; ++p; } |
| 206 |
3/4✓ Branch 0 taken 90 times.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
|
92 | if (p >= end || *p < '0' || *p > '9') fail("expected integer"); |
| 207 | 90 | int64_t v = 0; | |
| 208 |
5/6✓ Branch 0 taken 221 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 32 times.
✓ Branch 3 taken 189 times.
✓ Branch 4 taken 132 times.
✓ Branch 5 taken 89 times.
|
221 | while (p < end && *p >= '0' && *p <= '9') { |
| 209 | 132 | const int digit = *p - '0'; | |
| 210 |
3/4✓ Branch 0 taken 131 times.
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 time.
|
132 | if (v > (INT64_MAX - digit) / 10) fail("integer out of range"); |
| 211 | 131 | v = v * 10 + digit; | |
| 212 | 131 | ++p; | |
| 213 | } | ||
| 214 |
2/2✓ Branch 0 taken 1 time.
✓ Branch 1 taken 88 times.
|
89 | return neg ? -v : v; |
| 215 | 2 | } | |
| 216 | |||
| 217 | // Skip an arbitrary JSON value (used for __metadata__ entries we don't | ||
| 218 | // care about, and as a defensive fallback). | ||
| 219 | 8 | void skip_value() { | |
| 220 | 8 | skip_ws(); | |
| 221 |
1/4✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
|
8 | if (p >= end) fail("unexpected EOF"); |
| 222 |
2/2✓ Branch 0 taken 1 time.
✓ Branch 1 taken 7 times.
|
8 | if (*p == '"') { (void)parse_string(); return; } |
| 223 |
4/4✓ Branch 0 taken 4 times.
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 1 time.
✓ Branch 3 taken 3 times.
|
7 | if (*p == '{' || *p == '[') { |
| 224 | 4 | char open = *p++, close = (open == '{') ? '}' : ']'; | |
| 225 | 4 | int depth = 1; | |
| 226 |
3/4✗ Branch 0 not taken.
✓ Branch 1 taken 44 times.
✓ Branch 2 taken 40 times.
✓ Branch 3 taken 4 times.
|
44 | while (p < end && depth > 0) { |
| 227 |
2/2✓ Branch 0 taken 11 times.
✓ Branch 1 taken 29 times.
|
40 | if (*p == '"') { (void)parse_string(); continue; } |
| 228 |
2/2✓ Branch 0 taken 1 time.
✓ Branch 1 taken 28 times.
|
29 | if (*p == open) ++depth; |
| 229 |
2/2✓ Branch 0 taken 23 times.
✓ Branch 1 taken 5 times.
|
28 | else if (*p == close) --depth; |
| 230 | 29 | ++p; | |
| 231 | } | ||
| 232 | 4 | return; | |
| 233 | } | ||
| 234 | // number / true / false / null | ||
| 235 |
7/10✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✓ Branch 3 taken 3 times.
✓ Branch 4 taken 9 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 9 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 3 times.
✓ Branch 9 taken 9 times.
|
15 | while (p < end && *p != ',' && *p != '}' && *p != ']' && |
| 236 |
3/6✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 9 times.
|
18 | *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') ++p; |
| 237 | 8 | } | |
| 238 | }; | ||
| 239 | |||
| 240 | 64 | struct Entry { | |
| 241 | 32 | Dtype dtype = Dtype::Unknown; | |
| 242 | std::vector<int64_t> shape; | ||
| 243 | 32 | uint64_t off_start = 0; | |
| 244 | 32 | uint64_t off_end = 0; | |
| 245 | }; | ||
| 246 | |||
| 247 | 32 | Entry parse_entry(Parser& ps) { | |
| 248 | 32 | Entry e; | |
| 249 |
1/2✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
|
32 | ps.expect('{'); |
| 250 | 32 | bool first = true; | |
| 251 | 126 | while (true) { | |
| 252 | 126 | ps.skip_ws(); | |
| 253 |
2/2✓ Branch 0 taken 98 times.
✓ Branch 1 taken 28 times.
|
126 | if (ps.eat('}')) break; |
| 254 |
3/4✓ Branch 0 taken 66 times.
✓ Branch 1 taken 32 times.
✓ Branch 2 taken 66 times.
✗ Branch 3 not taken.
|
98 | if (!first) ps.expect(','); |
| 255 | 98 | first = false; | |
| 256 |
1/2✓ Branch 0 taken 98 times.
✗ Branch 1 not taken.
|
98 | std::string key = ps.parse_string(); |
| 257 |
1/2✓ Branch 0 taken 98 times.
✗ Branch 1 not taken.
|
98 | ps.expect(':'); |
| 258 |
2/2✓ Branch 0 taken 32 times.
✓ Branch 1 taken 66 times.
|
98 | if (key == "dtype") { |
| 259 |
1/2✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
|
32 | std::string v = ps.parse_string(); |
| 260 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 32 times.
|
32 | e.dtype = parse_dtype(v); |
| 261 |
2/2✓ Branch 0 taken 31 times.
✓ Branch 1 taken 1 time.
|
32 | if (e.dtype == Dtype::Unknown) { |
| 262 |
5/10✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 time.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 time.
✗ Branch 7 not taken.
✓ Branch 8 taken 1 time.
✗ Branch 9 not taken.
|
1 | throw std::runtime_error("safetensors: unsupported dtype '" + v + "'"); |
| 263 | } | ||
| 264 |
2/2✓ Branch 0 taken 31 times.
✓ Branch 1 taken 35 times.
|
98 | } else if (key == "shape") { |
| 265 |
1/2✓ Branch 0 taken 31 times.
✗ Branch 1 not taken.
|
31 | ps.expect('['); |
| 266 | 31 | bool sfirst = true; | |
| 267 | 63 | while (true) { | |
| 268 | 63 | ps.skip_ws(); | |
| 269 |
2/2✓ Branch 0 taken 29 times.
✓ Branch 1 taken 34 times.
|
63 | if (ps.eat(']')) break; |
| 270 |
3/4✓ Branch 0 taken 3 times.
✓ Branch 1 taken 31 times.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
|
34 | if (!sfirst) ps.expect(','); |
| 271 | 34 | sfirst = false; | |
| 272 |
3/4✓ Branch 0 taken 32 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 32 times.
✗ Branch 3 not taken.
|
34 | e.shape.push_back(ps.parse_int()); |
| 273 | } | ||
| 274 |
2/2✓ Branch 0 taken 29 times.
✓ Branch 1 taken 6 times.
|
64 | } else if (key == "data_offsets") { |
| 275 |
1/2✓ Branch 0 taken 29 times.
✗ Branch 1 not taken.
|
29 | ps.expect('['); |
| 276 |
1/2✓ Branch 0 taken 29 times.
✗ Branch 1 not taken.
|
29 | e.off_start = static_cast<uint64_t>(ps.parse_int()); |
| 277 |
2/2✓ Branch 0 taken 28 times.
✓ Branch 1 taken 1 time.
|
29 | ps.expect(','); |
| 278 |
1/2✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
|
28 | e.off_end = static_cast<uint64_t>(ps.parse_int()); |
| 279 |
1/2✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
|
28 | ps.expect(']'); |
| 280 | 28 | } else { | |
| 281 | // Unknown field — skip its value defensively. | ||
| 282 |
1/2✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
|
6 | ps.skip_value(); |
| 283 | } | ||
| 284 | 98 | } | |
| 285 | 28 | return e; | |
| 286 | 36 | } | |
| 287 | |||
| 288 | } // namespace | ||
| 289 | |||
| 290 | // ─── mmap (platform) ─────────────────────────────────────────────────────── | ||
| 291 | |||
| 292 | namespace { | ||
| 293 | |||
| 294 | 27 | struct Mapping { | |
| 295 | 27 | void* file = nullptr; | |
| 296 | 27 | void* mapping = nullptr; | |
| 297 | 27 | void* base = nullptr; | |
| 298 | 27 | std::size_t size = 0; | |
| 299 | }; | ||
| 300 | |||
| 301 | 27 | Mapping map_file(const std::string& path) { | |
| 302 | 27 | Mapping m; | |
| 303 | #ifdef _WIN32 | ||
| 304 | HANDLE fh = CreateFileA(path.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, | ||
| 305 | OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); | ||
| 306 | if (fh == INVALID_HANDLE_VALUE) { | ||
| 307 | throw std::runtime_error("safetensors: cannot open '" + path + "'"); | ||
| 308 | } | ||
| 309 | LARGE_INTEGER sz; | ||
| 310 | if (!GetFileSizeEx(fh, &sz)) { | ||
| 311 | CloseHandle(fh); | ||
| 312 | throw std::runtime_error("safetensors: GetFileSizeEx failed for '" + path + "'"); | ||
| 313 | } | ||
| 314 | HANDLE mh = CreateFileMappingA(fh, nullptr, PAGE_READONLY, 0, 0, nullptr); | ||
| 315 | if (!mh) { | ||
| 316 | CloseHandle(fh); | ||
| 317 | throw std::runtime_error("safetensors: CreateFileMapping failed for '" + path + "'"); | ||
| 318 | } | ||
| 319 | void* base = MapViewOfFile(mh, FILE_MAP_READ, 0, 0, 0); | ||
| 320 | if (!base) { | ||
| 321 | CloseHandle(mh); | ||
| 322 | CloseHandle(fh); | ||
| 323 | throw std::runtime_error("safetensors: MapViewOfFile failed for '" + path + "'"); | ||
| 324 | } | ||
| 325 | m.file = fh; | ||
| 326 | m.mapping = mh; | ||
| 327 | m.base = base; | ||
| 328 | m.size = static_cast<std::size_t>(sz.QuadPart); | ||
| 329 | #else | ||
| 330 | 27 | int fd = ::open(path.c_str(), O_RDONLY); | |
| 331 |
6/10✓ Branch 0 taken 26 times.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 time.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 time.
✗ Branch 7 not taken.
✓ Branch 8 taken 1 time.
✗ Branch 9 not taken.
|
27 | if (fd < 0) throw std::runtime_error("safetensors: cannot open '" + path + "'"); |
| 332 | 26 | struct stat st{}; | |
| 333 |
1/2✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
|
26 | if (fstat(fd, &st) != 0) { |
| 334 | ✗ | ::close(fd); | |
| 335 | ✗ | throw std::runtime_error("safetensors: fstat failed for '" + path + "'"); | |
| 336 | } | ||
| 337 | 26 | void* base = ::mmap(nullptr, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0); | |
| 338 |
1/2✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
|
26 | if (base == MAP_FAILED) { |
| 339 | ✗ | ::close(fd); | |
| 340 | ✗ | throw std::runtime_error("safetensors: mmap failed for '" + path + "'"); | |
| 341 | } | ||
| 342 | 26 | m.file = reinterpret_cast<void*>(static_cast<intptr_t>(fd)); | |
| 343 | 26 | m.mapping = nullptr; | |
| 344 | 26 | m.base = base; | |
| 345 | 26 | m.size = static_cast<std::size_t>(st.st_size); | |
| 346 | #endif | ||
| 347 | 26 | return m; | |
| 348 | 1 | } | |
| 349 | |||
| 350 | 30 | void unmap_file(Mapping& m) noexcept { | |
| 351 | #ifdef _WIN32 | ||
| 352 | if (m.base) UnmapViewOfFile(m.base); | ||
| 353 | if (m.mapping) CloseHandle(m.mapping); | ||
| 354 | if (m.file) CloseHandle(m.file); | ||
| 355 | #else | ||
| 356 |
4/6✓ Branch 0 taken 26 times.
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 26 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 26 times.
✗ Branch 5 not taken.
|
30 | if (m.base && m.size) ::munmap(m.base, m.size); |
| 357 |
3/4✓ Branch 0 taken 4 times.
✓ Branch 1 taken 26 times.
✓ Branch 2 taken 26 times.
✗ Branch 3 not taken.
|
30 | if (m.file) ::close(static_cast<int>(reinterpret_cast<intptr_t>(m.file))); |
| 358 | #endif | ||
| 359 | 30 | m = Mapping{}; | |
| 360 | 30 | } | |
| 361 | |||
| 362 | } // namespace | ||
| 363 | |||
| 364 | // ─── File ────────────────────────────────────────────────────────────────── | ||
| 365 | |||
| 366 | 50 | File::~File() { release_(); } | |
| 367 | |||
| 368 | 2 | File::File(File&& other) noexcept | |
| 369 | 1 | : file_(other.file_), mapping_(other.mapping_), base_(other.base_), | |
| 370 | 1 | file_size_(other.file_size_), | |
| 371 | 2 | tensors_(std::move(other.tensors_)), index_(std::move(other.index_)) { | |
| 372 | 1 | other.file_ = other.mapping_ = other.base_ = nullptr; | |
| 373 | 1 | other.file_size_ = 0; | |
| 374 | 2 | } | |
| 375 | |||
| 376 | 2 | File& File::operator=(File&& other) noexcept { | |
| 377 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
|
2 | if (this != &other) { |
| 378 | 2 | release_(); | |
| 379 | 2 | file_ = other.file_; | |
| 380 | 2 | mapping_ = other.mapping_; | |
| 381 | 2 | base_ = other.base_; | |
| 382 | 2 | file_size_ = other.file_size_; | |
| 383 | 2 | tensors_ = std::move(other.tensors_); | |
| 384 | 2 | index_ = std::move(other.index_); | |
| 385 | 2 | other.file_ = other.mapping_ = other.base_ = nullptr; | |
| 386 | 2 | other.file_size_ = 0; | |
| 387 | 2 | } | |
| 388 | 2 | return *this; | |
| 389 | } | ||
| 390 | |||
| 391 | 27 | void File::release_() noexcept { | |
| 392 | 27 | Mapping m{file_, mapping_, base_, file_size_}; | |
| 393 | 27 | unmap_file(m); | |
| 394 | 27 | file_ = mapping_ = base_ = nullptr; | |
| 395 | 27 | file_size_ = 0; | |
| 396 | 27 | tensors_.clear(); | |
| 397 | 27 | index_.clear(); | |
| 398 | 27 | } | |
| 399 | |||
| 400 | 26 | File File::open(const std::string& path) { | |
| 401 | 26 | Mapping m = map_file(path); | |
| 402 |
2/2✓ Branch 0 taken 25 times.
✓ Branch 1 taken 1 time.
|
26 | if (m.size < 8) { |
| 403 | 1 | unmap_file(m); | |
| 404 |
4/8✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 time.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 time.
✗ Branch 7 not taken.
|
14 | throw std::runtime_error("safetensors: file too small: '" + path + "'"); |
| 405 | } | ||
| 406 | 25 | const uint8_t* bytes = static_cast<const uint8_t*>(m.base); | |
| 407 | 25 | uint64_t header_size = 0; | |
| 408 | 25 | std::memcpy(&header_size, bytes, 8); // little-endian; platforms we target are LE | |
| 409 |
2/2✓ Branch 0 taken 23 times.
✓ Branch 1 taken 2 times.
|
25 | if (header_size > m.size - 8) { |
| 410 | 2 | unmap_file(m); | |
| 411 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | throw std::runtime_error("safetensors: header_size exceeds file"); |
| 412 | } | ||
| 413 | 23 | const char* hdr_begin = reinterpret_cast<const char*>(bytes + 8); | |
| 414 | 23 | const char* hdr_end = hdr_begin + header_size; | |
| 415 | 23 | const uint8_t* payload_base = bytes + 8 + header_size; | |
| 416 | 23 | std::size_t payload_size = m.size - 8 - header_size; | |
| 417 | |||
| 418 | 23 | File f; | |
| 419 | 23 | f.file_ = m.file; | |
| 420 | 23 | f.mapping_ = m.mapping; | |
| 421 | 23 | f.base_ = m.base; | |
| 422 | 23 | f.file_size_ = m.size; | |
| 423 | |||
| 424 | 23 | Parser ps{hdr_begin, hdr_end}; | |
| 425 | try { | ||
| 426 |
2/2✓ Branch 0 taken 22 times.
✓ Branch 1 taken 1 time.
|
23 | ps.expect('{'); |
| 427 | 22 | bool first = true; | |
| 428 | 48 | while (true) { | |
| 429 |
1/2✓ Branch 0 taken 48 times.
✗ Branch 1 not taken.
|
48 | ps.skip_ws(); |
| 430 |
3/4✓ Branch 0 taken 48 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 38 times.
✓ Branch 3 taken 10 times.
|
48 | if (ps.eat('}')) break; |
| 431 |
3/4✓ Branch 0 taken 18 times.
✓ Branch 1 taken 20 times.
✓ Branch 2 taken 18 times.
✗ Branch 3 not taken.
|
38 | if (!first) ps.expect(','); |
| 432 | 38 | first = false; | |
| 433 |
2/2✓ Branch 0 taken 35 times.
✓ Branch 1 taken 3 times.
|
38 | std::string name = ps.parse_string(); |
| 434 |
2/2✓ Branch 0 taken 34 times.
✓ Branch 1 taken 1 time.
|
35 | ps.expect(':'); |
| 435 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 32 times.
|
34 | if (name == "__metadata__") { |
| 436 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | ps.skip_value(); |
| 437 | 2 | continue; | |
| 438 | } | ||
| 439 |
2/2✓ Branch 0 taken 28 times.
✓ Branch 1 taken 4 times.
|
32 | Entry e = parse_entry(ps); |
| 440 |
2/2✓ Branch 0 taken 26 times.
✓ Branch 1 taken 2 times.
|
28 | if (e.off_end < e.off_start || e.off_end > payload_size) { |
| 441 |
2/4✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
|
4 | throw std::runtime_error( |
| 442 |
2/4✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
|
2 | "safetensors: tensor '" + name + "' has out-of-range data_offsets"); |
| 443 | } | ||
| 444 | 26 | TensorView tv; | |
| 445 |
1/2✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
|
26 | tv.name = name; |
| 446 | 26 | tv.dtype = e.dtype; | |
| 447 | 26 | tv.shape = std::move(e.shape); | |
| 448 | 26 | tv.data = payload_base + e.off_start; | |
| 449 | 26 | tv.nbytes = static_cast<std::size_t>(e.off_end - e.off_start); | |
| 450 | |||
| 451 |
2/2✓ Branch 0 taken 25 times.
✓ Branch 1 taken 1 time.
|
26 | int64_t expected = tv.numel() * dtype_size_bytes(tv.dtype); |
| 452 |
2/2✓ Branch 0 taken 24 times.
✓ Branch 1 taken 1 time.
|
25 | if (expected != static_cast<int64_t>(tv.nbytes)) { |
| 453 |
2/4✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
|
2 | throw std::runtime_error( |
| 454 |
2/4✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
|
1 | "safetensors: tensor '" + name + |
| 455 | "' nbytes/shape mismatch"); | ||
| 456 | } | ||
| 457 |
1/2✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
|
24 | f.index_.emplace(tv.name, f.tensors_.size()); |
| 458 |
1/2✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
|
24 | f.tensors_.push_back(std::move(tv)); |
| 459 | 35 | } | |
| 460 | 23 | } catch (...) { | |
| 461 | // f's destructor will unmap. | ||
| 462 | 13 | throw; | |
| 463 | 13 | } | |
| 464 | 10 | return f; | |
| 465 | 37 | } | |
| 466 | |||
| 467 | 36 | const TensorView* File::find(std::string_view name) const { | |
| 468 | 36 | auto it = index_.find(name); | |
| 469 |
2/2✓ Branch 0 taken 5 times.
✓ Branch 1 taken 31 times.
|
36 | if (it == index_.end()) return nullptr; |
| 470 | 31 | return &tensors_[it->second]; | |
| 471 | 36 | } | |
| 472 | |||
| 473 | 5 | const TensorView& File::get(std::string_view name) const { | |
| 474 | 5 | const TensorView* v = find(name); | |
| 475 |
2/2✓ Branch 0 taken 3 times.
✓ Branch 1 taken 2 times.
|
5 | if (!v) { |
| 476 |
3/6✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
|
4 | throw std::runtime_error( |
| 477 |
3/6✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
|
2 | "safetensors: no tensor named '" + std::string(name) + "'"); |
| 478 | } | ||
| 479 | 3 | return *v; | |
| 480 | 2 | } | |
| 481 | |||
| 482 | // ─── Upload ──────────────────────────────────────────────────────────────── | ||
| 483 | |||
| 484 | 9 | void upload(const TensorView& view, int rows, int cols, brotensor::Tensor& dst) { | |
| 485 |
2/2✓ Branch 0 taken 7 times.
✓ Branch 1 taken 2 times.
|
9 | if (rows <= 0 || cols <= 0) { |
| 486 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
4 | throw std::runtime_error("safetensors::upload: rows/cols must be positive"); |
| 487 | } | ||
| 488 | 7 | const std::size_t expected = | |
| 489 | 14 | static_cast<std::size_t>(rows) * static_cast<std::size_t>(cols) * | |
| 490 | 7 | static_cast<std::size_t>(dtype_size_bytes(view.dtype)); | |
| 491 |
2/2✓ Branch 0 taken 6 times.
✓ Branch 1 taken 1 time.
|
7 | if (expected != view.nbytes) { |
| 492 |
3/6✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 time.
✗ Branch 5 not taken.
|
2 | throw std::runtime_error( |
| 493 |
2/4✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
|
1 | "safetensors::upload: byte count mismatch for tensor '" + view.name + "'"); |
| 494 | } | ||
| 495 |
2/2✓ Branch 0 taken 3 times.
✓ Branch 1 taken 3 times.
|
6 | if (view.dtype == Dtype::F32) { |
| 496 | 3 | dst = brotensor::Tensor::from_host(reinterpret_cast<const float*>(view.data), rows, cols); | |
| 497 |
2/2✓ Branch 0 taken 1 time.
✓ Branch 1 taken 2 times.
|
6 | } else if (view.dtype == Dtype::F16) { |
| 498 | 1 | dst = brotensor::Tensor::from_host_fp16( | |
| 499 | 1 | reinterpret_cast<const uint16_t*>(view.data), rows, cols); | |
| 500 |
2/2✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1 time.
|
3 | } else if (view.dtype == Dtype::BF16) { |
| 501 | 1 | dst = brotensor::Tensor::from_host_bf16( | |
| 502 | 1 | reinterpret_cast<const uint16_t*>(view.data), rows, cols); | |
| 503 | 1 | } else { | |
| 504 |
2/4✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
|
2 | throw std::runtime_error( |
| 505 |
2/4✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
|
1 | std::string("safetensors::upload: unsupported dtype ") + |
| 506 |
3/6✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 time.
✗ Branch 5 not taken.
|
1 | dtype_name(view.dtype) + " for tensor '" + view.name + "'"); |
| 507 | } | ||
| 508 | 7 | } | |
| 509 | |||
| 510 | 5 | void upload_fp16(const TensorView& view, int rows, int cols, brotensor::Tensor& dst) { | |
| 511 |
2/2✓ Branch 0 taken 4 times.
✓ Branch 1 taken 1 time.
|
5 | if (rows <= 0 || cols <= 0) { |
| 512 |
1/2✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
|
3 | throw std::runtime_error("safetensors::upload_fp16: rows/cols must be positive"); |
| 513 | } | ||
| 514 | 4 | const std::size_t n = static_cast<std::size_t>(rows) * static_cast<std::size_t>(cols); | |
| 515 | 4 | const std::size_t expected = n * static_cast<std::size_t>(dtype_size_bytes(view.dtype)); | |
| 516 |
2/2✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1 time.
|
4 | if (expected != view.nbytes) { |
| 517 |
3/6✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 time.
✗ Branch 5 not taken.
|
2 | throw std::runtime_error( |
| 518 |
2/4✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
|
1 | "safetensors::upload_fp16: byte count mismatch for tensor '" + view.name + "'"); |
| 519 | } | ||
| 520 |
2/2✓ Branch 0 taken 1 time.
✓ Branch 1 taken 2 times.
|
3 | if (view.dtype == Dtype::F16) { |
| 521 | 1 | dst = brotensor::Tensor::from_host_fp16( | |
| 522 | 1 | reinterpret_cast<const uint16_t*>(view.data), rows, cols); | |
| 523 |
2/2✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1 time.
|
3 | } else if (view.dtype == Dtype::F32) { |
| 524 | 1 | const float* src = reinterpret_cast<const float*>(view.data); | |
| 525 | 1 | std::vector<uint16_t> tmp(n); | |
| 526 |
2/2✓ Branch 0 taken 1 time.
✓ Branch 1 taken 6 times.
|
7 | for (std::size_t i = 0; i < n; ++i) { |
| 527 |
1/2✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
|
6 | tmp[i] = brotensor::fp32_to_fp16_bits(src[i]); |
| 528 | 6 | } | |
| 529 |
1/2✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
|
1 | dst = brotensor::Tensor::from_host_fp16(tmp.data(), rows, cols); |
| 530 | 1 | } else { | |
| 531 |
2/4✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
|
2 | throw std::runtime_error( |
| 532 |
2/4✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
|
1 | std::string("safetensors::upload_fp16: unsupported dtype ") + |
| 533 |
3/6✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 time.
✗ Branch 5 not taken.
|
1 | dtype_name(view.dtype) + " for tensor '" + view.name + "'"); |
| 534 | } | ||
| 535 | 4 | } | |
| 536 | |||
| 537 | 21 | void upload_as(const TensorView& view, int rows, int cols, | |
| 538 | brotensor::Dtype want, brotensor::Tensor& dst) { | ||
| 539 |
2/2✓ Branch 0 taken 20 times.
✓ Branch 1 taken 1 time.
|
21 | if (rows <= 0 || cols <= 0) { |
| 540 |
1/2✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
|
3 | throw std::runtime_error("safetensors::upload_as: rows/cols must be positive"); |
| 541 | } | ||
| 542 | 20 | const std::size_t n = static_cast<std::size_t>(rows) * static_cast<std::size_t>(cols); | |
| 543 | 20 | const std::size_t expected = n * static_cast<std::size_t>(dtype_size_bytes(view.dtype)); | |
| 544 |
2/2✓ Branch 0 taken 19 times.
✓ Branch 1 taken 1 time.
|
20 | if (expected != view.nbytes) { |
| 545 |
3/6✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 time.
✗ Branch 5 not taken.
|
2 | throw std::runtime_error( |
| 546 |
2/4✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
|
1 | "safetensors::upload_as: byte count mismatch for tensor '" + view.name + "'"); |
| 547 | } | ||
| 548 |
6/6✓ Branch 0 taken 13 times.
✓ Branch 1 taken 6 times.
✓ Branch 2 taken 8 times.
✓ Branch 3 taken 5 times.
✓ Branch 4 taken 7 times.
✓ Branch 5 taken 1 time.
|
19 | if (view.dtype != Dtype::F16 && view.dtype != Dtype::F32 && |
| 549 | 8 | view.dtype != Dtype::BF16) { | |
| 550 |
2/4✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
|
2 | throw std::runtime_error( |
| 551 |
2/4✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
|
1 | std::string("safetensors::upload_as: unsupported source dtype ") + |
| 552 |
3/6✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 time.
✗ Branch 5 not taken.
|
1 | dtype_name(view.dtype) + " for tensor '" + view.name + "'"); |
| 553 | } | ||
| 554 | |||
| 555 | // Decode any source element to FP32. | ||
| 556 | 18 | const float* src32 = reinterpret_cast<const float*>(view.data); | |
| 557 | 18 | const uint16_t* src16 = reinterpret_cast<const uint16_t*>(view.data); | |
| 558 | |||
| 559 | // GPU fast path: when the on-disk dtype differs from the compute dtype | ||
| 560 | // (e.g. BF16 checkpoints with an FP16 compute policy), upload the raw bytes | ||
| 561 | // at the source dtype and cast on-device. The host convert-then-upload path | ||
| 562 | // below is a single-threaded scalar loop over every element — for a 2.6B-param | ||
| 563 | // encoder that is ~13 s, versus a ~0.1 s device cast after the same H2D copy. | ||
| 564 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 18 times.
|
18 | if (brotensor::default_device() != brotensor::Device::CPU) { |
| 565 | ✗ | brotensor::Tensor raw; | |
| 566 | ✗ | switch (view.dtype) { | |
| 567 | ✗ | case Dtype::F32: raw = brotensor::Tensor::from_host(src32, rows, cols); break; | |
| 568 | ✗ | case Dtype::F16: raw = brotensor::Tensor::from_host_fp16(src16, rows, cols); break; | |
| 569 | ✗ | default: raw = brotensor::Tensor::from_host_bf16(src16, rows, cols); break; | |
| 570 | } | ||
| 571 | ✗ | if (raw.dtype == want) { | |
| 572 | ✗ | dst = std::move(raw); | |
| 573 | ✗ | } else { | |
| 574 | ✗ | brotensor::cast(raw, dst, want); | |
| 575 | } | ||
| 576 | return; | ||
| 577 | ✗ | } | |
| 578 | 80 | auto at_f32 = [&](std::size_t i) -> float { | |
| 579 |
3/3✓ Branch 0 taken 26 times.
✓ Branch 1 taken 24 times.
✓ Branch 2 taken 12 times.
|
62 | switch (view.dtype) { |
| 580 | 12 | case Dtype::F32: return src32[i]; | |
| 581 | 26 | case Dtype::F16: return brotensor::fp16_bits_to_fp32(src16[i]); | |
| 582 | 24 | default: return brotensor::bf16_bits_to_fp32(src16[i]); | |
| 583 | } | ||
| 584 | 62 | }; | |
| 585 | |||
| 586 |
4/4✓ Branch 0 taken 10 times.
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 1 time.
|
18 | switch (want) { |
| 587 | case brotensor::Dtype::FP32: { | ||
| 588 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 8 times.
|
10 | if (view.dtype == Dtype::F32) { |
| 589 | 2 | dst = brotensor::Tensor::from_host(src32, rows, cols); | |
| 590 | 2 | return; | |
| 591 | } | ||
| 592 | 8 | std::vector<float> tmp(n); | |
| 593 |
3/4✓ Branch 0 taken 8 times.
✓ Branch 1 taken 38 times.
✓ Branch 2 taken 38 times.
✗ Branch 3 not taken.
|
46 | for (std::size_t i = 0; i < n; ++i) tmp[i] = at_f32(i); |
| 594 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
8 | dst = brotensor::Tensor::from_host(tmp.data(), rows, cols); |
| 595 | return; | ||
| 596 | 8 | } | |
| 597 | case brotensor::Dtype::FP16: { | ||
| 598 |
2/2✓ Branch 0 taken 1 time.
✓ Branch 1 taken 2 times.
|
3 | if (view.dtype == Dtype::F16) { |
| 599 | 1 | dst = brotensor::Tensor::from_host_fp16(src16, rows, cols); | |
| 600 | 1 | return; | |
| 601 | } | ||
| 602 | 2 | std::vector<uint16_t> tmp(n); | |
| 603 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 12 times.
|
14 | for (std::size_t i = 0; i < n; ++i) { |
| 604 |
2/4✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
|
12 | tmp[i] = brotensor::fp32_to_fp16_bits(at_f32(i)); |
| 605 | 12 | } | |
| 606 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | dst = brotensor::Tensor::from_host_fp16(tmp.data(), rows, cols); |
| 607 | return; | ||
| 608 | 2 | } | |
| 609 | case brotensor::Dtype::BF16: { | ||
| 610 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
|
4 | if (view.dtype == Dtype::BF16) { |
| 611 | 2 | dst = brotensor::Tensor::from_host_bf16(src16, rows, cols); | |
| 612 | 2 | return; | |
| 613 | } | ||
| 614 | 2 | std::vector<uint16_t> tmp(n); | |
| 615 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 12 times.
|
14 | for (std::size_t i = 0; i < n; ++i) { |
| 616 |
2/4✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
|
12 | tmp[i] = brotensor::fp32_to_bf16_bits(at_f32(i)); |
| 617 | 12 | } | |
| 618 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | dst = brotensor::Tensor::from_host_bf16(tmp.data(), rows, cols); |
| 619 | return; | ||
| 620 | 2 | } | |
| 621 | default: | ||
| 622 |
1/2✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
|
1 | throw std::runtime_error( |
| 623 | "safetensors::upload_as: target dtype must be FP32/FP16/BF16"); | ||
| 624 | } | ||
| 625 | 19 | } | |
| 626 | |||
| 627 | 7 | void upload_compute(const TensorView& view, int rows, int cols, | |
| 628 | brotensor::Tensor& dst) { | ||
| 629 | // The compute dtype follows the active brotensor device: FP32 on the CPU | ||
| 630 | // backend, FP16 on a GPU backend. BF16 in particular is the on-disk dtype | ||
| 631 | // of Flux-family weights; CPU has no BF16 arithmetic, so it is widened to | ||
| 632 | // FP32 there rather than carried through as BF16. | ||
| 633 | 7 | upload_as(view, rows, cols, brotensor::compute_dtype(), dst); | |
| 634 | 7 | } | |
| 635 | |||
| 636 | 4 | void upload_compute_checked(const TensorView& view, int rows, int cols, | |
| 637 | brotensor::Tensor& dst, const std::string& name) { | ||
| 638 |
6/6✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 1 time.
✓ Branch 4 taken 1 time.
✓ Branch 5 taken 1 time.
|
4 | if (view.dtype != Dtype::F16 && view.dtype != Dtype::F32 && |
| 639 | 2 | view.dtype != Dtype::BF16) { | |
| 640 |
3/6✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 time.
✗ Branch 5 not taken.
|
3 | throw std::runtime_error( |
| 641 |
4/8✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 time.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 time.
✗ Branch 7 not taken.
|
1 | name + " ('" + view.name + "'): expected F16, F32, or BF16, got " + |
| 642 | 1 | dtype_name(view.dtype)); | |
| 643 | } | ||
| 644 | 3 | const int64_t expected = | |
| 645 | 3 | static_cast<int64_t>(rows) * static_cast<int64_t>(cols); | |
| 646 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 time.
|
3 | if (view.numel() != expected) { |
| 647 |
2/4✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
|
2 | throw std::runtime_error( |
| 648 |
4/8✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 time.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 time.
✗ Branch 7 not taken.
|
2 | name + " ('" + view.name + "'): shape mismatch (expected " + |
| 649 |
6/12✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 time.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 time.
✗ Branch 7 not taken.
✓ Branch 8 taken 1 time.
✗ Branch 9 not taken.
✓ Branch 10 taken 1 time.
✗ Branch 11 not taken.
|
3 | std::to_string(rows) + "x" + std::to_string(cols) + ", got " + |
| 650 |
3/6✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 time.
✓ Branch 4 taken 1 time.
✗ Branch 5 not taken.
|
2 | std::to_string(view.numel()) + " elements)"); |
| 651 | } | ||
| 652 | 2 | upload_compute(view, rows, cols, dst); | |
| 653 | 4 | } | |
| 654 | |||
| 655 | // ─── Writer ──────────────────────────────────────────────────────────────── | ||
| 656 | |||
| 657 | namespace { | ||
| 658 | |||
| 659 | 19 | const char* dtype_safetensors_name(Dtype d) { | |
| 660 |
7/8✓ Branch 0 taken 4 times.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 1 time.
✓ Branch 4 taken 1 time.
✓ Branch 5 taken 9 times.
✓ Branch 6 taken 1 time.
✗ Branch 7 not taken.
|
19 | switch (d) { |
| 661 | 4 | case Dtype::F32: return "F32"; | |
| 662 | 1 | case Dtype::F16: return "F16"; | |
| 663 | 2 | case Dtype::BF16: return "BF16"; | |
| 664 | 1 | case Dtype::I32: return "I32"; | |
| 665 | 1 | case Dtype::I64: return "I64"; | |
| 666 | 9 | case Dtype::U8: return "U8"; | |
| 667 | 1 | case Dtype::BOOL: return "BOOL"; | |
| 668 | ✗ | default: throw std::runtime_error("safetensors::write: unsupported dtype"); | |
| 669 | } | ||
| 670 | 19 | } | |
| 671 | |||
| 672 | 19 | void json_escape_append(std::string& out, const std::string& s) { | |
| 673 |
2/2✓ Branch 0 taken 114 times.
✓ Branch 1 taken 19 times.
|
133 | for (char c : s) { |
| 674 |
8/8✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 1 time.
✓ Branch 3 taken 107 times.
✓ Branch 4 taken 1 time.
✓ Branch 5 taken 1 time.
✓ Branch 6 taken 1 time.
✓ Branch 7 taken 1 time.
|
114 | switch (c) { |
| 675 | 1 | case '"': out += "\\\""; break; | |
| 676 | 1 | case '\\': out += "\\\\"; break; | |
| 677 | 1 | case '\n': out += "\\n"; break; | |
| 678 | 1 | case '\t': out += "\\t"; break; | |
| 679 | 1 | case '\r': out += "\\r"; break; | |
| 680 | 1 | case '\b': out += "\\b"; break; | |
| 681 | 1 | case '\f': out += "\\f"; break; | |
| 682 | default: | ||
| 683 |
2/2✓ Branch 0 taken 1 time.
✓ Branch 1 taken 106 times.
|
107 | if (static_cast<unsigned char>(c) < 0x20) { |
| 684 | char buf[8]; | ||
| 685 | 2 | std::snprintf(buf, sizeof(buf), "\\u%04x", | |
| 686 | 1 | static_cast<unsigned>(static_cast<unsigned char>(c))); | |
| 687 | 1 | out += buf; | |
| 688 | 1 | } else { | |
| 689 | 106 | out += c; | |
| 690 | } | ||
| 691 | 107 | } | |
| 692 | } | ||
| 693 | 19 | } | |
| 694 | |||
| 695 | } // namespace | ||
| 696 | |||
| 697 | 13 | void write_file(const std::string& path, const std::vector<WriteEntry>& entries) { | |
| 698 | // Validate + compute offsets. | ||
| 699 |
1/2✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
|
13 | std::vector<uint64_t> off_start(entries.size()), off_end(entries.size()); |
| 700 | 13 | uint64_t cursor = 0; | |
| 701 | 13 | std::unordered_set<std::string> seen_names; | |
| 702 |
2/2✓ Branch 0 taken 28 times.
✓ Branch 1 taken 6 times.
|
34 | for (std::size_t i = 0; i < entries.size(); ++i) { |
| 703 | 28 | const WriteEntry& e = entries[i]; | |
| 704 |
2/2✓ Branch 0 taken 27 times.
✓ Branch 1 taken 1 time.
|
28 | if (e.name.empty()) { |
| 705 |
2/4✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
|
1 | throw std::runtime_error("safetensors::write: empty tensor name"); |
| 706 | } | ||
| 707 |
3/4✓ Branch 0 taken 27 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 26 times.
✓ Branch 3 taken 1 time.
|
27 | if (!seen_names.insert(e.name).second) { |
| 708 |
4/8✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 time.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 time.
✗ Branch 7 not taken.
|
3 | throw std::runtime_error("safetensors::write: duplicate tensor name '" + |
| 709 |
1/2✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
|
1 | e.name + "'"); |
| 710 | } | ||
| 711 |
4/4✓ Branch 0 taken 2 times.
✓ Branch 1 taken 24 times.
✓ Branch 2 taken 1 time.
✓ Branch 3 taken 1 time.
|
26 | if (!e.host_data && e.bytes > 0) { |
| 712 |
4/8✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 time.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 time.
✗ Branch 7 not taken.
|
1 | throw std::runtime_error("safetensors::write: '" + e.name + "' null data"); |
| 713 | } | ||
| 714 | 25 | int dsz = dtype_size_bytes(e.dtype); | |
| 715 |
2/2✓ Branch 0 taken 23 times.
✓ Branch 1 taken 2 times.
|
25 | if (dsz <= 0) { |
| 716 |
4/8✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
|
2 | throw std::runtime_error("safetensors::write: '" + e.name + "' bad dtype"); |
| 717 | } | ||
| 718 | // Same overflow guard the reader applies in TensorView::numel() — an | ||
| 719 | // unchecked product here is signed-overflow UB, not a clean throw. | ||
| 720 | 23 | int64_t n = 1; | |
| 721 |
2/2✓ Branch 0 taken 25 times.
✓ Branch 1 taken 22 times.
|
47 | for (int64_t d : e.shape) { |
| 722 |
6/10✓ Branch 0 taken 24 times.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 time.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 time.
✗ Branch 7 not taken.
✓ Branch 8 taken 1 time.
✗ Branch 9 not taken.
|
25 | if (d < 0) throw std::runtime_error("safetensors::write: '" + e.name + "' negative shape"); |
| 723 |
3/4✓ Branch 0 taken 23 times.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 23 times.
✗ Branch 3 not taken.
|
24 | if (d != 0 && n > INT64_MAX / d) |
| 724 | ✗ | throw std::runtime_error("safetensors::write: '" + e.name + "' element count overflows int64"); | |
| 725 | 24 | n *= d; | |
| 726 | } | ||
| 727 | 22 | std::size_t expected = static_cast<std::size_t>(n) * static_cast<std::size_t>(dsz); | |
| 728 |
2/2✓ Branch 0 taken 21 times.
✓ Branch 1 taken 1 time.
|
22 | if (expected != e.bytes) { |
| 729 |
4/8✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 time.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 time.
✗ Branch 7 not taken.
|
2 | throw std::runtime_error("safetensors::write: '" + e.name + |
| 730 |
3/6✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 time.
✗ Branch 5 not taken.
|
1 | "' byte count mismatch (shape=" + std::to_string(expected) + |
| 731 |
3/6✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 time.
✗ Branch 5 not taken.
|
1 | " bytes=" + std::to_string(e.bytes) + ")"); |
| 732 | } | ||
| 733 | 21 | off_start[i] = cursor; | |
| 734 | 21 | cursor += e.bytes; | |
| 735 | 21 | off_end[i] = cursor; | |
| 736 | 21 | } | |
| 737 | |||
| 738 | // Build JSON header. | ||
| 739 | 6 | std::string hdr; | |
| 740 |
1/2✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
|
6 | hdr.reserve(256 * entries.size() + 16); |
| 741 |
1/2✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
|
6 | hdr += '{'; |
| 742 |
2/2✓ Branch 0 taken 6 times.
✓ Branch 1 taken 19 times.
|
25 | for (std::size_t i = 0; i < entries.size(); ++i) { |
| 743 |
3/4✓ Branch 0 taken 14 times.
✓ Branch 1 taken 5 times.
✓ Branch 2 taken 14 times.
✗ Branch 3 not taken.
|
19 | if (i) hdr += ','; |
| 744 |
1/2✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
|
19 | hdr += '"'; |
| 745 |
1/2✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
|
19 | json_escape_append(hdr, entries[i].name); |
| 746 |
1/2✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
|
19 | hdr += "\":{\"dtype\":\""; |
| 747 |
2/4✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 19 times.
✗ Branch 3 not taken.
|
19 | hdr += dtype_safetensors_name(entries[i].dtype); |
| 748 |
1/2✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
|
19 | hdr += "\",\"shape\":["; |
| 749 |
2/2✓ Branch 0 taken 19 times.
✓ Branch 1 taken 21 times.
|
40 | for (std::size_t j = 0; j < entries[i].shape.size(); ++j) { |
| 750 |
3/4✓ Branch 0 taken 2 times.
✓ Branch 1 taken 19 times.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
|
21 | if (j) hdr += ','; |
| 751 |
2/4✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 21 times.
|
21 | hdr += std::to_string(entries[i].shape[j]); |
| 752 | 21 | } | |
| 753 |
1/2✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
|
19 | hdr += "],\"data_offsets\":["; |
| 754 |
2/4✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 19 times.
✗ Branch 3 not taken.
|
19 | hdr += std::to_string(off_start[i]); |
| 755 |
1/2✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
|
19 | hdr += ','; |
| 756 |
2/4✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 19 times.
✗ Branch 3 not taken.
|
19 | hdr += std::to_string(off_end[i]); |
| 757 |
1/2✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
|
19 | hdr += "]}"; |
| 758 | 19 | } | |
| 759 |
1/2✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
|
6 | hdr += '}'; |
| 760 | // Pad header to 8-byte alignment so payload tensor starts are aligned. | ||
| 761 |
3/4✓ Branch 0 taken 6 times.
✓ Branch 1 taken 23 times.
✓ Branch 2 taken 23 times.
✗ Branch 3 not taken.
|
29 | while ((hdr.size() % 8) != 0) hdr += ' '; |
| 762 | |||
| 763 | 6 | uint64_t header_size = static_cast<uint64_t>(hdr.size()); | |
| 764 | |||
| 765 | // Write file. | ||
| 766 | #ifdef _WIN32 | ||
| 767 | FILE* fp = nullptr; | ||
| 768 | fopen_s(&fp, path.c_str(), "wb"); | ||
| 769 | #else | ||
| 770 | // open(..., 0644) rather than fopen: fopen requests 0666 and leans on the | ||
| 771 | // caller's umask to take the write bits back off. A process that has | ||
| 772 | // cleared its umask — a daemon, a container entrypoint — then writes a | ||
| 773 | // world-writable checkpoint, which anyone on the box can swap under us. | ||
| 774 | // Ask for 0644 outright; umask can only ever narrow it further. | ||
| 775 |
1/2✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
|
6 | const int fd = ::open(path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644); |
| 776 |
3/4✓ Branch 0 taken 5 times.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
|
6 | FILE* fp = (fd < 0) ? nullptr : ::fdopen(fd, "wb"); |
| 777 |
3/6✓ Branch 0 taken 5 times.
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
✓ Branch 3 taken 5 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
|
6 | if (fd >= 0 && !fp) ::close(fd); // fdopen failed: the fd is still ours |
| 778 | #endif | ||
| 779 |
6/10✓ Branch 0 taken 5 times.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 time.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 time.
✗ Branch 7 not taken.
✓ Branch 8 taken 1 time.
✗ Branch 9 not taken.
|
6 | if (!fp) throw std::runtime_error("safetensors::write: cannot open '" + path + "'"); |
| 780 | |||
| 781 | 5 | auto wfail = [&](const std::string& what) { | |
| 782 | ✗ | std::fclose(fp); | |
| 783 | ✗ | throw std::runtime_error("safetensors::write: " + what + " '" + path + "'"); | |
| 784 | ✗ | }; | |
| 785 | |||
| 786 |
2/8✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 5 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
|
5 | if (std::fwrite(&header_size, 1, 8, fp) != 8) wfail("header_size write failed"); |
| 787 |
2/8✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 5 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
|
5 | if (std::fwrite(hdr.data(), 1, hdr.size(), fp) != hdr.size()) wfail("header write failed"); |
| 788 |
2/2✓ Branch 0 taken 5 times.
✓ Branch 1 taken 18 times.
|
23 | for (const WriteEntry& e : entries) { |
| 789 |
2/2✓ Branch 0 taken 17 times.
✓ Branch 1 taken 1 time.
|
18 | if (e.bytes == 0) continue; |
| 790 |
2/8✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 17 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
|
17 | if (std::fwrite(e.host_data, 1, e.bytes, fp) != e.bytes) wfail("payload write failed"); |
| 791 | } | ||
| 792 |
1/2✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
|
5 | std::fclose(fp); |
| 793 | 20 | } | |
| 794 | |||
| 795 | } // namespace brotensor::safetensors | ||
| 796 |