convert_ds41_upstream.py
| 1 | #!/usr/bin/env python3 |
| 2 | """Convert the ISTA-DASLab DeepSeek-V4.1-Flash-1017 GSQ-RCO GGUFs to llama.cpp's own deepseek41 layout. |
| 3 | |
| 4 | The files published before October 27, 2026 were written for ISTA's llama.cpp branch. llama.cpp's DeepSeek-V4.1 |
| 5 | support (ggml-org/llama.cpp#31086, merged October 25, 2026) reads the layout of its converter |
| 6 | (conversion/deepseek41.py). This script writes that layout from the old files: |
| 7 | |
| 8 | - metadata under the upstream key names; the Engram hash constants (token map, bucket counts, multipliers) move |
| 9 | from tensors into metadata, with absolute row offsets into the concatenated table |
| 10 | - the Engram tables of all Engram layers become one tensor, per_layer_token_embd. They stay in the shared |
| 11 | Engram file, which is converted in place: only its 416-byte header is rewritten, the 110 GB of table data stay |
| 12 | where they are. llama.cpp opens it as shard 6 of each build, through a link <build>-00006-of-00006.gguf |
| 13 | - blk.N.engram_wkv is renamed blk.N.engram_kv; blk.N.engram_q and blk.N.engram_k are folded into the F32 |
| 14 | blk.N.engram_gate = q * k (exact: a product of two BF16 values fits in F32) |
| 15 | - the vision router bias blk.N.exp_probs_b_vl is added (the repository's exp_probs_b_vl.f32, F32 from the |
| 16 | original checkpoint); llama.cpp routes image tokens with it |
| 17 | - blk.N.attn_output_a as the 2D matrix of the checkpoint (it was stored as [4096, 1024, 8]; same bytes) |
| 18 | - tensors in converter order, split like llama-gguf-split --split-max-size 40G --no-tensor-first-split: shard 1 |
| 19 | holds only metadata, so later metadata fixes replace a 6 MB file |
| 20 | - the chat template of the repository's chat_template.jinja, so downloads that were never patched get the |
| 21 | current one too |
| 22 | - mmproj: projector type deepseek41v (V4.1 image token layout) instead of deepseek4v |
| 23 | |
| 24 | Quantized weights are copied byte for byte. Every written file is read back: each tensor is compared with its |
| 25 | source, and the file with the SHA-256 of the published one (the Engram tables too, before the header is rewritten), |
| 26 | so a damaged old download or an unfinished patch_chat_template.py run stops the conversion with an error instead of |
| 27 | giving wrong files. Standard library only (Python 3.8+). The model is not downloaded again; |
| 28 | the script only fetches chat_template.jinja and exp_probs_b_vl.f32 (80 KB) from the repository, unless their current |
| 29 | versions (checked by SHA-256) lie next to it or in the model folder (then it runs offline). |
| 30 | |
| 31 | python3 convert_ds41_upstream.py MODEL_FOLDER [--out NEW_FOLDER] |
| 32 | |
| 33 | MODEL_FOLDER holds the old shards (*-0000N-of-00004.gguf), DeepSeek-V4.1-Flash-Engram-IQ4_NL.gguf and optionally the |
| 34 | mmproj. Every build found there is converted: its new shards 1 to 5 are written next to the old ones (or to --out) |
| 35 | and need as much space as its old shards (121 to 155 GB). With --delete-old, the old shards of a build are deleted as |
| 36 | soon as its new ones are verified, so free space for one build suffices. On a folder of new builds the script only |
| 37 | adds missing links to the Engram file. |
| 38 | """ |
| 39 | import argparse |
| 40 | import hashlib |
| 41 | import os |
| 42 | import re |
| 43 | import struct |
| 44 | import sys |
| 45 | import urllib.request |
| 46 | |
| 47 | REPO = 'ISTA-DASLab/DeepSeek-V4.1-Flash-1017-GSQ-RCO-GGUF' # chat_template.jinja and exp_probs_b_vl.f32 come from here |
| 48 | OLD_REVISION = '653a9874aa38c356233c5f3cc8c96b19a43b57eb' # the repository's last revision with the old files |
| 49 | BUILD_PREFIX = 'DeepSeek-V4.1-Flash-1017-GSQ-RCO-' |
| 50 | ARCH = 'deepseek41' |
| 51 | TABLE = 'per_layer_token_embd.weight' |
| 52 | ENGRAM_FILE = 'DeepSeek-V4.1-Flash-Engram-IQ4_NL.gguf' |
| 53 | N_SPLIT = 6 # shards per build; the last is the shared Engram file |
| 54 | SPLIT_MAX_SIZE = 40 * 1000 ** 3 # llama-gguf-split --split-max-size 40G |
| 55 | ALIGNMENT = 32 |
| 56 | SPLIT_RE = re.compile(r'^(?P<prefix>.+)-(?P<no>\d{5})-of-(?P<count>\d{5})\.gguf$') |
| 57 | COPY_CHUNK = 64 << 20 |
| 58 | |
| 59 | # --------------------------------------------------------------------------------------------------------------- |
| 60 | # GGUF header reading and writing |
| 61 | |
| 62 | GGUF_MAGIC = 0x46554747 |
| 63 | T_U16, T_U32, T_I32, T_STRING, T_ARRAY, T_U64 = 2, 4, 5, 8, 9, 10 |
| 64 | SCALAR = {0: '<B', 1: '<b', 2: '<H', 3: '<h', 4: '<I', 5: '<i', 6: '<f', 7: '<?', 10: '<Q', 11: '<q', 12: '<d'} |
| 65 | GGML_F32, GGML_IQ4_NL, GGML_I32, GGML_I64, GGML_BF16 = 0, 20, 26, 27, 30 |
| 66 | # GGML type -> (elements per block, bytes per block), for the types these files contain |
| 67 | GGML_BLOCK = {0: (1, 4), 1: (1, 2), 8: (32, 34), 10: (256, 84), 11: (256, 110), 12: (256, 144), 13: (256, 176), |
| 68 | 14: (256, 210), 16: (256, 66), 17: (256, 74), 18: (256, 98), 19: (256, 50), 20: (32, 18), |
| 69 | 21: (256, 110), 22: (256, 82), 23: (256, 136), 26: (1, 4), 27: (1, 8), 29: (256, 56), 30: (1, 2), |
| 70 | 39: (32, 17)} |
| 71 | |
| 72 | |
| 73 | def align(n, a=ALIGNMENT): |
| 74 | return (n + a - 1) // a * a |
| 75 | |
| 76 | |
| 77 | def tensor_nbytes(dims, ggml_type): |
| 78 | per_block, block_bytes = GGML_BLOCK[ggml_type] |
| 79 | n = 1 |
| 80 | for d in dims: |
| 81 | n *= d |
| 82 | if dims[0] % per_block: |
| 83 | raise ValueError('row of %d elements is not a whole number of type %d blocks' % (dims[0], ggml_type)) |
| 84 | return n // per_block * block_bytes |
| 85 | |
| 86 | |
| 87 | class _Reader: |
| 88 | def __init__(self, buf): |
| 89 | self.buf, self.p = buf, 0 |
| 90 | |
| 91 | def take(self, n): |
| 92 | if self.p + n > len(self.buf): |
| 93 | raise EOFError |
| 94 | b = self.buf[self.p:self.p + n] |
| 95 | self.p += n |
| 96 | return b |
| 97 | |
| 98 | def u(self, fmt): |
| 99 | return struct.unpack(fmt, self.take(struct.calcsize(fmt)))[0] |
| 100 | |
| 101 | def string(self): |
| 102 | return self.take(self.u('<Q')).decode('utf-8') |
| 103 | |
| 104 | def skip_value(self, t): |
| 105 | if t == T_STRING: |
| 106 | self.take(self.u('<Q')) |
| 107 | elif t == T_ARRAY: |
| 108 | at, n = self.u('<I'), self.u('<Q') |
| 109 | if at in (T_STRING, T_ARRAY): |
| 110 | for _ in range(n): |
| 111 | self.skip_value(at) |
| 112 | else: |
| 113 | self.take(n * struct.calcsize(SCALAR[at])) |
| 114 | else: |
| 115 | self.take(struct.calcsize(SCALAR[t])) |
| 116 | |
| 117 | |
| 118 | def decode_value(t, raw): |
| 119 | r = _Reader(raw) |
| 120 | if t == T_STRING: |
| 121 | return r.string() |
| 122 | if t == T_ARRAY: |
| 123 | at, n = r.u('<I'), r.u('<Q') |
| 124 | if at == T_STRING: |
| 125 | return [r.string() for _ in range(n)] |
| 126 | fmt = SCALAR[at] |
| 127 | return list(struct.unpack('<%d%s' % (n, fmt[1]), r.take(n * struct.calcsize(fmt)))) |
| 128 | return struct.unpack(SCALAR[t], raw)[0] |
| 129 | |
| 130 | |
| 131 | def encode_value(t, value, array_type=None): |
| 132 | if t == T_STRING: |
| 133 | b = value.encode('utf-8') |
| 134 | return struct.pack('<Q', len(b)) + b |
| 135 | if t == T_ARRAY: |
| 136 | head = struct.pack('<IQ', array_type, len(value)) |
| 137 | if array_type == T_STRING: |
| 138 | return head + b''.join(encode_value(T_STRING, v) for v in value) |
| 139 | return head + struct.pack('<%d%s' % (len(value), SCALAR[array_type][1]), *value) |
| 140 | return struct.pack(SCALAR[t], value) |
| 141 | |
| 142 | |
| 143 | class GGUFFile: |
| 144 | """Header of a GGUF file: kv is a list of [key, type, raw value bytes], tensors maps a name to |
| 145 | (absolute file offset, dims, ggml type, byte size).""" |
| 146 | |
| 147 | def __init__(self, path): |
| 148 | self.path = path |
| 149 | size = 1 << 20 |
| 150 | while True: |
| 151 | with open(path, 'rb') as f: |
| 152 | buf = f.read(size) |
| 153 | try: |
| 154 | self._parse(buf) |
| 155 | break |
| 156 | except EOFError: |
| 157 | if len(buf) < size: |
| 158 | raise ValueError('%s: truncated GGUF header' % path) |
| 159 | size *= 4 |
| 160 | self.size = os.path.getsize(path) |
| 161 | |
| 162 | def _parse(self, buf): |
| 163 | r = _Reader(buf) |
| 164 | if r.u('<I') != GGUF_MAGIC: |
| 165 | raise ValueError('%s is not a GGUF file' % self.path) |
| 166 | self.version = r.u('<I') |
| 167 | n_tensors, n_kv = r.u('<Q'), r.u('<Q') |
| 168 | self.kv = [] |
| 169 | for _ in range(n_kv): |
| 170 | key = r.string() |
| 171 | t = r.u('<I') |
| 172 | start = r.p |
| 173 | r.skip_value(t) |
| 174 | self.kv.append([key, t, buf[start:r.p]]) |
| 175 | infos = [] |
| 176 | for _ in range(n_tensors): |
| 177 | name = r.string() |
| 178 | dims = [r.u('<Q') for _ in range(r.u('<I'))] |
| 179 | infos.append((name, dims, r.u('<I'), r.u('<Q'))) |
| 180 | self.header_size = r.p |
| 181 | self.data_start = align(r.p, self.get('general.alignment', ALIGNMENT)) |
| 182 | self.tensors = {} |
| 183 | self.order = [] |
| 184 | for name, dims, gt, off in infos: |
| 185 | self.tensors[name] = (self.data_start + off, dims, gt, tensor_nbytes(dims, gt)) |
| 186 | self.order.append(name) |
| 187 | |
| 188 | def get(self, key, default=None): |
| 189 | for k, t, raw in self.kv: |
| 190 | if k == key: |
| 191 | return decode_value(t, raw) |
| 192 | return default |
| 193 | |
| 194 | def read_tensor(self, name): |
| 195 | off, _, _, n = self.tensors[name] |
| 196 | with open(self.path, 'rb') as f: |
| 197 | f.seek(off) |
| 198 | data = f.read(n) |
| 199 | if len(data) != n: |
| 200 | raise ValueError('%s: tensor %s is truncated' % (self.path, name)) |
| 201 | return data |
| 202 | |
| 203 | |
| 204 | def serialize_header(kv, infos): |
| 205 | """kv: [key, type, raw]; infos: (name, dims, ggml type, relative offset). Padded to the data section.""" |
| 206 | out = [struct.pack('<IIQQ', GGUF_MAGIC, 3, len(infos), len(kv))] |
| 207 | for key, t, raw in kv: |
| 208 | out += [encode_value(T_STRING, key), struct.pack('<I', t), raw] |
| 209 | for name, dims, gt, off in infos: |
| 210 | out += [encode_value(T_STRING, name), struct.pack('<I', len(dims)), |
| 211 | struct.pack('<%dQ' % len(dims), *dims), struct.pack('<IQ', gt, off)] |
| 212 | header = b''.join(out) |
| 213 | return header + b'\0' * (align(len(header)) - len(header)) |
| 214 | |
| 215 | |
| 216 | # --------------------------------------------------------------------------------------------------------------- |
| 217 | # conversion |
| 218 | |
| 219 | |
| 220 | class Tensor: |
| 221 | """An output tensor: bytes from a file range or computed here.""" |
| 222 | |
| 223 | def __init__(self, name, dims, ggml_type, path=None, offset=None, data=None): |
| 224 | self.name, self.dims, self.type = name, list(dims), ggml_type |
| 225 | self.nbytes = tensor_nbytes(self.dims, ggml_type) |
| 226 | self.path, self.offset, self.data = path, offset, data |
| 227 | if data is not None and len(data) != self.nbytes: |
| 228 | raise ValueError('%s: %d bytes for a tensor of %d' % (name, len(data), self.nbytes)) |
| 229 | |
| 230 | def source_hash(self): |
| 231 | if self.data is not None: |
| 232 | return hashlib.sha256(self.data).hexdigest() |
| 233 | return hash_range(self.path, self.offset, self.nbytes) |
| 234 | |
| 235 | |
| 236 | def read_into(f, n, *hashes): |
| 237 | """Reads the next n bytes of f into the given hashes.""" |
| 238 | while n: |
| 239 | b = f.read(min(n, COPY_CHUNK)) |
| 240 | if not b: |
| 241 | raise ValueError('%s ends early' % f.name) |
| 242 | for h in hashes: |
| 243 | h.update(b) |
| 244 | n -= len(b) |
| 245 | |
| 246 | |
| 247 | def hash_range(path, offset, n): |
| 248 | h = hashlib.sha256() |
| 249 | with open(path, 'rb') as f: |
| 250 | f.seek(offset) |
| 251 | read_into(f, n, h) |
| 252 | return h.hexdigest() |
| 253 | |
| 254 | |
| 255 | # the old branch-specific metadata, replaced by the upstream keys |
| 256 | OLD_KEYS = ['attention.candidate_block_size', 'attention.candidate_topk_blocks', 'attention.candidate_source_layer_id', |
| 257 | 'kv_source_layer_ids', 'index_source_layer_ids', 'dspark_target_layer_ids', 'expert_scoring_func', |
| 258 | 'engram.present', 'engram.layer_ids', 'engram.num_embeddings', 'engram_head_dim', 'engram_n_heads', |
| 259 | 'engram_max_ngram_size', 'engram_vocab_size', 'engram_compressed_vocab_size', 'engram_pad_token_id', |
| 260 | 'engram.source', 'engram.file'] |
| 261 | CONST_TENSORS = ['engram_token_map.weight', 'engram_primes.weight', 'engram_offsets.weight', |
| 262 | 'engram_multipliers.weight'] |
| 263 | |
| 264 | |
| 265 | def unpack(data, fmt): |
| 266 | return list(struct.unpack('<%d%s' % (len(data) // struct.calcsize(fmt), fmt[1]), data)) |
| 267 | |
| 268 | |
| 269 | def convert_metadata(meta, tensor_source, chat_template): |
| 270 | """meta: GGUFFile with the old first-shard metadata. Returns the new kv list (without split keys).""" |
| 271 | A = ARCH + '.' |
| 272 | get = lambda k: meta.get(A + k) |
| 273 | if meta.get('general.architecture') != ARCH: |
| 274 | raise ValueError('%s: architecture %r, expected %s' % (meta.path, meta.get('general.architecture'), ARCH)) |
| 275 | missing = [A + k for k in OLD_KEYS if meta.get(A + k) is None and k not in ('dspark_target_layer_ids', |
| 276 | 'expert_scoring_func')] |
| 277 | if missing: |
| 278 | raise ValueError('%s: missing metadata %s' % (meta.path, ', '.join(missing))) |
| 279 | |
| 280 | layers = get('engram.layer_ids') |
| 281 | n_gram, n_heads, head_dim = get('engram_max_ngram_size'), get('engram_n_heads'), get('engram_head_dim') |
| 282 | n_cols = (n_gram - 1) * n_heads |
| 283 | token_map = unpack(tensor_source('engram_token_map.weight'), '<i') |
| 284 | primes = unpack(tensor_source('engram_primes.weight'), '<q') |
| 285 | local_offsets = unpack(tensor_source('engram_offsets.weight'), '<q') |
| 286 | multipliers = unpack(tensor_source('engram_multipliers.weight'), '<q') |
| 287 | if len(primes) != len(layers) * n_cols or len(local_offsets) != len(primes) or \ |
| 288 | len(multipliers) != len(layers) * n_gram: |
| 289 | raise ValueError('Engram constants do not match %d layers x %d heads' % (len(layers), n_cols)) |
| 290 | for e, rows in enumerate(get('engram.num_embeddings')): |
| 291 | layer_primes = primes[e * n_cols:(e + 1) * n_cols] |
| 292 | if sum(layer_primes) != rows: |
| 293 | raise ValueError('Engram layer %d: bucket counts do not add up to its table rows' % layers[e]) |
| 294 | if local_offsets[e * n_cols:(e + 1) * n_cols] != [sum(layer_primes[:i]) for i in range(n_cols)]: |
| 295 | raise ValueError('Engram layer %d: unexpected row offsets' % layers[e]) |
| 296 | if max(token_map) + 1 != get('engram_compressed_vocab_size') or \ |
| 297 | len(token_map) != len(meta.get('tokenizer.ggml.tokens')): |
| 298 | raise ValueError('Engram token map does not match the vocabulary') |
| 299 | if any(m <= 0 or m % 2 == 0 for m in multipliers): |
| 300 | raise ValueError('Engram multipliers must be odd and positive') |
| 301 | |
| 302 | new = [ |
| 303 | ('attention.kv_source_layers', T_ARRAY, get('kv_source_layer_ids'), T_U32), |
| 304 | ('attention.indexer.source_layers', T_ARRAY, get('index_source_layer_ids'), T_U32), |
| 305 | ('attention.indexer.candidate_source_layer', T_I32, get('attention.candidate_source_layer_id'), None), |
| 306 | ('attention.indexer.candidate_top_k_blocks', T_U32, get('attention.candidate_topk_blocks'), None), |
| 307 | ('attention.indexer.candidate_block_size', T_U32, get('attention.candidate_block_size'), None), |
| 308 | ('ple.layers', T_ARRAY, layers, T_U32), |
| 309 | ('ple.ngram_size', T_U32, n_gram, None), |
| 310 | ('ple.heads_per_ngram', T_U32, n_heads, None), |
| 311 | ('embedding_length_per_layer_input', T_U32, head_dim, None), |
| 312 | ('ple.layer_multipliers', T_ARRAY, multipliers, T_U64), |
| 313 | ('ple.head_offsets', T_ARRAY, [sum(primes[:i]) for i in range(len(primes))], T_U64), |
| 314 | ('ple.head_vocab_sizes', T_ARRAY, primes, T_U64), |
| 315 | ('engram.pad_token_id', T_U32, get('engram_pad_token_id'), None), |
| 316 | ('engram.token_map', T_ARRAY, token_map, T_I32), |
| 317 | ] |
| 318 | old = {A + k for k in OLD_KEYS} |
| 319 | kv, inserted = [], False |
| 320 | for key, t, raw in meta.kv: |
| 321 | if key.startswith('split.'): |
| 322 | continue |
| 323 | if key in old: |
| 324 | if not inserted: |
| 325 | kv += [[A + k, vt, encode_value(vt, v, at)] for k, vt, v, at in new] |
| 326 | inserted = True |
| 327 | continue |
| 328 | if key == 'tokenizer.chat_template': |
| 329 | raw = encode_value(T_STRING, chat_template) # the repository's, whether or not the old one was patched |
| 330 | kv.append([key, t, raw]) |
| 331 | return kv |
| 332 | |
| 333 | |
| 334 | def bf16_to_f32(h): |
| 335 | return struct.unpack('<f', struct.pack('<I', h << 16))[0] |
| 336 | |
| 337 | |
| 338 | def engram_gate(q, k): |
| 339 | """F32 q * k from two BF16 tensors (the upstream converter's data_torch.float() * k().float()).""" |
| 340 | n = len(q) // 2 |
| 341 | qv, kv = struct.unpack('<%dH' % n, q), struct.unpack('<%dH' % n, k) |
| 342 | return struct.pack('<%df' % n, *[bf16_to_f32(a) * bf16_to_f32(b) for a, b in zip(qv, kv)]) |
| 343 | |
| 344 | |
| 345 | def canonical_key(name): |
| 346 | """Converter order: global tensors, then the blocks in order, the Engram table last; names sorted.""" |
| 347 | if name == 'per_layer_token_embd.weight': |
| 348 | return (2, 0, name) |
| 349 | m = re.match(r'blk\.(\d+)\.', name) |
| 350 | return (1, int(m.group(1)), name) if m else (0, 0, name) |
| 351 | |
| 352 | |
| 353 | def plan_tensors(meta, sources, engram, bias_vl): |
| 354 | """sources: GGUFFiles holding the old backbone tensors; engram: the old Engram GGUFFile.""" |
| 355 | index = {} |
| 356 | for src in sources: |
| 357 | for name, (off, dims, gt, _) in src.tensors.items(): |
| 358 | if name in index: |
| 359 | raise ValueError('tensor %s found in %s and %s' % (name, index[name][0].path, src.path)) |
| 360 | index[name] = (src, off, dims, gt) |
| 361 | A = ARCH + '.' |
| 362 | n_layer, n_expert = meta.get(A + 'block_count'), meta.get(A + 'expert_count') |
| 363 | layers = meta.get(A + 'engram.layer_ids') |
| 364 | out = [] |
| 365 | for name, (src, off, dims, gt) in index.items(): |
| 366 | if name in CONST_TENSORS or re.match(r'blk\.\d+\.engram_[qk]\.weight$', name): |
| 367 | continue |
| 368 | if name.endswith('.attn_output_a.weight') and len(dims) == 3: |
| 369 | dims = [dims[0], dims[1] * dims[2]] # the checkpoint's 2D wo_a, as the converter writes it; same bytes |
| 370 | out.append(Tensor(name.replace('.engram_wkv.', '.engram_kv.'), dims, gt, src.path, off)) |
| 371 | |
| 372 | for layer in layers: |
| 373 | q, k = ('blk.%d.engram_%s.weight' % (layer, w) for w in 'qk') |
| 374 | if index[q][3] != GGML_BF16 or index[k][3] != GGML_BF16 or index[q][2] != index[k][2]: |
| 375 | raise ValueError('blk.%d engram_q / engram_k must be BF16 of the same shape' % layer) |
| 376 | out.append(Tensor('blk.%d.engram_gate.weight' % layer, index[q][2], GGML_F32, |
| 377 | data=engram_gate(index[q][0].read_tensor(q), index[k][0].read_tensor(k)))) |
| 378 | |
| 379 | if len(bias_vl) != n_layer * n_expert * 4: |
| 380 | raise ValueError('exp_probs_b_vl.f32 is for a different model') |
| 381 | for layer in range(n_layer): |
| 382 | if 'blk.%d.exp_probs_b.bias' % layer not in index: |
| 383 | raise ValueError('blk.%d.exp_probs_b.bias is missing' % layer) |
| 384 | row = bias_vl[layer * n_expert * 4:(layer + 1) * n_expert * 4] |
| 385 | out.append(Tensor('blk.%d.exp_probs_b_vl.bias' % layer, [n_expert], GGML_F32, data=row)) |
| 386 | |
| 387 | table, table_layers = engram_table(engram) |
| 388 | if table.dims[1] != sum(meta.get(A + 'engram.num_embeddings')) or table_layers not in (None, layers) or \ |
| 389 | meta.get(A + 'engram.source') != engram.get(A + 'engram.source'): |
| 390 | raise ValueError('%s does not belong to this model (other tables or checkpoint revision)' % engram.path) |
| 391 | out.append(table) |
| 392 | out.sort(key=lambda t: canonical_key(t.name)) |
| 393 | return out |
| 394 | |
| 395 | |
| 396 | def engram_table(engram): |
| 397 | """The Engram table as one tensor, and the layers of the old per-layer tables (None if already converted). |
| 398 | The old file holds the per-layer tables back to back, so their concatenation is one byte range.""" |
| 399 | if TABLE in engram.tensors: |
| 400 | off, dims, gt, _ = engram.tensors[TABLE] |
| 401 | return Tensor(TABLE, dims, gt, engram.path, off), None |
| 402 | names = sorted((n for n in engram.tensors if re.match(r'blk\.\d+\.engram_embd\.weight$', n)), |
| 403 | key=lambda n: engram.tensors[n][0]) |
| 404 | if not names or len(names) != len(engram.tensors): |
| 405 | raise ValueError('%s: unexpected tensors in the Engram file' % engram.path) |
| 406 | first_off, dims, gt, _ = engram.tensors[names[0]] |
| 407 | end, rows = first_off, 0 |
| 408 | for name in names: |
| 409 | off, d, g, n = engram.tensors[name] |
| 410 | if off != end or g != gt or d[0] != dims[0]: |
| 411 | raise ValueError('%s: the Engram tables are not contiguous' % engram.path) |
| 412 | end, rows = off + n, rows + d[1] |
| 413 | layers = [int(re.match(r'blk\.(\d+)\.', n).group(1)) for n in names] |
| 414 | return Tensor(TABLE, [dims[0], rows], gt, engram.path, first_off), layers |
| 415 | |
| 416 | |
| 417 | def plan_splits(tensors, max_size): |
| 418 | """llama-gguf-split --split-max-size with --no-tensor-first-split: the first file holds only metadata.""" |
| 419 | splits, cur = [[], []], 0 |
| 420 | for t in tensors: |
| 421 | n = align(t.nbytes) |
| 422 | if cur + n > max_size: |
| 423 | if not splits[-1]: |
| 424 | raise ValueError('tensor %s is larger than the split size and comes first' % t.name) |
| 425 | splits.append([]) |
| 426 | cur = n |
| 427 | else: |
| 428 | cur += n |
| 429 | splits[-1].append(t) |
| 430 | return splits |
| 431 | |
| 432 | |
| 433 | def split_kv(no, count, n_tensors): |
| 434 | return [['split.no', T_U16, encode_value(T_U16, no)], ['split.count', T_U16, encode_value(T_U16, count)], |
| 435 | ['split.tensors.count', T_I32, encode_value(T_I32, n_tensors)]] |
| 436 | |
| 437 | |
| 438 | def copy_range(src_path, offset, n, fout, buf): |
| 439 | """Plain reads and writes through buf, as on every file system (os.copy_file_range leaves the copy to the |
| 440 | file system's own implementation).""" |
| 441 | with open(src_path, 'rb') as fin: |
| 442 | fin.seek(offset) |
| 443 | while n: |
| 444 | got = fin.readinto(buf[:min(n, len(buf))]) |
| 445 | if not got: |
| 446 | raise ValueError('%s ends early' % src_path) |
| 447 | fout.write(buf[:got]) |
| 448 | n -= got |
| 449 | |
| 450 | |
| 451 | def write_gguf(path, kv, tensors): |
| 452 | infos, off = [], 0 |
| 453 | for t in tensors: |
| 454 | infos.append((t.name, t.dims, t.type, off)) |
| 455 | off += align(t.nbytes) |
| 456 | tmp = path + '.partial' |
| 457 | buf = memoryview(bytearray(COPY_CHUNK)) |
| 458 | with open(tmp, 'wb') as f: |
| 459 | f.write(serialize_header(kv, infos)) |
| 460 | for t in tensors: |
| 461 | if t.data is not None: |
| 462 | f.write(t.data) |
| 463 | else: |
| 464 | copy_range(t.path, t.offset, t.nbytes, f, buf) |
| 465 | f.write(b'\0' * (align(t.nbytes) - t.nbytes)) |
| 466 | f.flush() |
| 467 | os.fsync(f.fileno()) |
| 468 | os.replace(tmp, path) |
| 469 | |
| 470 | |
| 471 | def verify_gguf(path, kv, tensors, expected=None, origin=()): |
| 472 | """Re-read the written file: same metadata, tensor table and tensor bytes as planned, and the SHA-256 of the |
| 473 | published file (expected; None for a file this repository does not publish). origin: the old files it was made |
| 474 | from, for the error message.""" |
| 475 | g = GGUFFile(path) |
| 476 | if g.kv != kv or g.order != [t.name for t in tensors]: |
| 477 | raise ValueError('%s: header differs from the plan' % path) |
| 478 | if g.size != g.data_start + sum(align(t.nbytes) for t in tensors): |
| 479 | raise ValueError('%s: unexpected file size' % path) |
| 480 | whole = hashlib.sha256() |
| 481 | with open(path, 'rb') as f: |
| 482 | read_into(f, g.data_start, whole) |
| 483 | for t in tensors: |
| 484 | off, dims, gt, n = g.tensors[t.name] |
| 485 | if dims != t.dims or gt != t.type or off != f.tell(): |
| 486 | raise ValueError('%s: tensor %s differs from the plan' % (path, t.name)) |
| 487 | part = hashlib.sha256() |
| 488 | read_into(f, n, whole, part) |
| 489 | if part.hexdigest() != t.source_hash(): |
| 490 | raise ValueError('%s: tensor %s differs from its source%s. The copy or a read failed on this machine, ' |
| 491 | 'or the source changed during the conversion; a damaged download would have been ' |
| 492 | 'copied as it is' % (path, t.name, '' if t.path is None else ' (%s at %d)' |
| 493 | % (t.path, t.offset))) |
| 494 | read_into(f, align(n) - n, whole) |
| 495 | if expected is not None and whole.hexdigest() != expected: |
| 496 | raise ValueError('%s: every tensor equals its source, but the file is not the published one (SHA-256), so the ' |
| 497 | 'old files it was made from are not the published ones: %s. A damaged download, or a ' |
| 498 | 'patch_chat_template.py run that did not finish, gives this; download the new files instead' |
| 499 | % (path, ', '.join(os.path.basename(p) for p in origin))) |
| 500 | |
| 501 | |
| 502 | def plan_model(meta, sources, engram, chat_template, bias_vl): |
| 503 | """Metadata, all tensors (the Engram table last) and the backbone split of one build.""" |
| 504 | kv = convert_metadata(meta, lambda t: next(s for s in sources if t in s.tensors).read_tensor(t), chat_template) |
| 505 | tensors = plan_tensors(meta, sources, engram, bias_vl) |
| 506 | splits = plan_splits(tensors[:-1], SPLIT_MAX_SIZE) |
| 507 | if tensors[-1].name != TABLE or len(splits) != N_SPLIT - 1: |
| 508 | raise ValueError('the backbone needs %d shards; the shared Engram file is shard %d of %d' |
| 509 | % (len(splits), N_SPLIT, N_SPLIT)) |
| 510 | return kv, tensors, splits |
| 511 | |
| 512 | |
| 513 | def convert_model(meta, sources, engram, chat_template, bias_vl, name, out_dir, verify, log): |
| 514 | """Writes shards 1 to N_SPLIT - 1 of a build; shard N_SPLIT is the shared Engram file.""" |
| 515 | kv, tensors, splits = plan_model(meta, sources, engram, chat_template, bias_vl) |
| 516 | names = ['%s-%05d-of-%05d.gguf' % (name, i + 1, N_SPLIT) for i in range(len(splits))] |
| 517 | if verify and not all(n in PUBLISHED for n in names): |
| 518 | log(' %s is not a build of this repository: each tensor is checked against its source, but the files cannot ' |
| 519 | 'be checked against published ones' % name) |
| 520 | paths = [] |
| 521 | for i, part in enumerate(splits): |
| 522 | path = os.path.join(out_dir, names[i]) |
| 523 | part_kv = (kv if i == 0 else []) + split_kv(i, N_SPLIT, len(tensors)) |
| 524 | log(' %s: %d tensors, %.2f GB' % (names[i], len(part), sum(t.nbytes for t in part) / 1e9)) |
| 525 | write_gguf(path, part_kv, part) |
| 526 | if verify: |
| 527 | origin = [meta.path] if i == 0 else sorted({t.path for t in part if t.path is not None}) |
| 528 | verify_gguf(path, part_kv, part, PUBLISHED.get(names[i]), origin) |
| 529 | paths.append(path) |
| 530 | return paths, tensors |
| 531 | |
| 532 | |
| 533 | def engram_header(table, source, n_tensors): |
| 534 | kv = [['general.name', T_STRING, encode_value(T_STRING, 'DeepSeek-V4.1-Flash-1017 Engram tables')], |
| 535 | ['general.description', T_STRING, encode_value(T_STRING, 'Shard 6 of 6 of every GSQ-RCO build')], |
| 536 | [ARCH + '.engram.source', T_STRING, encode_value(T_STRING, source)]] |
| 537 | kv += split_kv(N_SPLIT - 1, N_SPLIT, n_tensors) |
| 538 | return kv, serialize_header(kv, [(TABLE, table.dims, table.type, 0)]) |
| 539 | |
| 540 | |
| 541 | def convert_engram(engram, table, n_tensors, verify, log): |
| 542 | """Rewrite the old Engram file's header in place. The new header ends where the old one did, so the 110 GB of |
| 543 | table data stay where they are; the old header is kept in a side file until the new one is checked. With verify, |
| 544 | the tables are first checked against the published file: its SHA-256 is that of the new header and the tables.""" |
| 545 | path = engram.path |
| 546 | backup = path + '.old-header' |
| 547 | kv, header = engram_header(table, engram.get(ARCH + '.engram.source'), n_tensors) |
| 548 | if TABLE in engram.tensors: |
| 549 | if engram.kv != kv or engram.data_start != len(header): |
| 550 | raise ValueError('%s: unexpected header' % path) |
| 551 | if os.path.exists(backup): |
| 552 | os.remove(backup) |
| 553 | return False |
| 554 | if len(header) != engram.data_start or table.offset != engram.data_start: |
| 555 | raise ValueError('%s: the new header does not fit the old one' % path) |
| 556 | if verify: |
| 557 | log('%s: checking the tables against the published file (%.0f GB)' |
| 558 | % (os.path.basename(path), engram.size / 1e9)) |
| 559 | h = hashlib.sha256(header) |
| 560 | with open(path, 'rb') as f: |
| 561 | f.seek(engram.data_start) |
| 562 | read_into(f, engram.size - engram.data_start, h) |
| 563 | if h.hexdigest() != PUBLISHED[ENGRAM_FILE]: |
| 564 | raise ValueError('%s: its tables are not those of the published file (SHA-256), e.g. from a damaged ' |
| 565 | 'download; the file was left unchanged. Download %s again' % (path, ENGRAM_FILE)) |
| 566 | with open(path, 'rb') as f: |
| 567 | old = f.read(engram.data_start) |
| 568 | with open(backup, 'wb') as f: |
| 569 | f.write(old) |
| 570 | os.fsync(f.fileno()) |
| 571 | with open(path, 'r+b') as f: |
| 572 | f.write(header) |
| 573 | f.flush() |
| 574 | os.fsync(f.fileno()) |
| 575 | g = GGUFFile(path) |
| 576 | if g.kv != kv or g.order != [TABLE] or g.tensors[TABLE][:3] != (table.offset, table.dims, table.type) or \ |
| 577 | g.size != engram.size: |
| 578 | with open(path, 'r+b') as f: |
| 579 | f.write(old) |
| 580 | raise ValueError('%s: the rewritten header did not check out; the old one was restored' % path) |
| 581 | os.remove(backup) |
| 582 | log('%s: header rewritten in place (%d bytes), the table data are unchanged' % (os.path.basename(path), len(old))) |
| 583 | return True |
| 584 | |
| 585 | |
| 586 | def link_engram(engram_path, name, out_dir, log): |
| 587 | """llama.cpp opens <name>-0000N-of-0000N.gguf as the last shard: a link to the shared Engram file.""" |
| 588 | path = os.path.join(out_dir, '%s-%05d-of-%05d.gguf' % (name, N_SPLIT, N_SPLIT)) |
| 589 | if os.path.lexists(path): |
| 590 | if os.path.exists(path) and os.path.samefile(path, engram_path): |
| 591 | return |
| 592 | raise ValueError('%s exists and is not the Engram file' % path) |
| 593 | try: |
| 594 | os.symlink(os.path.relpath(engram_path, out_dir), path) |
| 595 | kind = 'link' |
| 596 | except (OSError, NotImplementedError): |
| 597 | os.link(engram_path, path) # e.g. Windows without symlink rights |
| 598 | kind = 'hard link' |
| 599 | log(' %s: %s to %s' % (os.path.basename(path), kind, os.path.basename(engram_path))) |
| 600 | |
| 601 | |
| 602 | def convert_mmproj(src, out_path, name, log): |
| 603 | """name: the file name the result gets (out_path may be a temporary name).""" |
| 604 | if src.get('clip.projector_type') != 'deepseek4v': |
| 605 | raise ValueError('%s: projector type %r' % (src.path, src.get('clip.projector_type'))) |
| 606 | kv = [] |
| 607 | for key, t, raw in src.kv: |
| 608 | if key == 'clip.projector_type': |
| 609 | raw = encode_value(T_STRING, 'deepseek41v') |
| 610 | elif key == 'clip.vision.image_max_pixels': |
| 611 | continue # deepseek41v fixes the image budget at 1024 tokens; the upstream converter writes no key |
| 612 | kv.append([key, t, raw]) |
| 613 | tensors = [Tensor(n, src.tensors[n][1], src.tensors[n][2], src.path, src.tensors[n][0]) for n in src.order] |
| 614 | log(' %s: projector type deepseek41v' % name) |
| 615 | if name not in PUBLISHED: |
| 616 | log(' %s is not a file of this repository: each tensor is checked against its source, but the file cannot ' |
| 617 | 'be checked against a published one' % name) |
| 618 | write_gguf(out_path, kv, tensors) |
| 619 | verify_gguf(out_path, kv, tensors, PUBLISHED.get(name), [src.path]) |
| 620 | |
| 621 | |
| 622 | # --------------------------------------------------------------------------------------------------------------- |
| 623 | # discovery |
| 624 | |
| 625 | |
| 626 | def find_sets(paths): |
| 627 | files = [] |
| 628 | for p in paths: |
| 629 | if os.path.isdir(p): |
| 630 | files += [os.path.join(p, f) for f in sorted(os.listdir(p)) if f.endswith('.gguf')] |
| 631 | else: |
| 632 | files.append(p) |
| 633 | models, shards, engrams, mmprojs, converted, firsts = [], {}, {}, [], [], set() |
| 634 | for path in files: |
| 635 | for p in {path, os.path.realpath(path)}: |
| 636 | journal = os.path.join(os.path.dirname(p), '.%s.chat-template-recovery' % os.path.basename(p)) |
| 637 | if os.path.isdir(journal): |
| 638 | raise ValueError('%s: a patch_chat_template.py run on this file did not finish (%s exists), so its ' |
| 639 | 'tensor data are partly moved. Finish it by running the same command again (the ' |
| 640 | 'script is in revision %s of the repository), or download the new files' |
| 641 | % (path, journal, OLD_REVISION)) |
| 642 | g = GGUFFile(path) |
| 643 | arch = g.get('general.architecture') |
| 644 | m = SPLIT_RE.match(os.path.basename(path)) |
| 645 | if arch == ARCH and m: |
| 646 | firsts.add((os.path.dirname(path), m.group('prefix'))) |
| 647 | if arch == ARCH: |
| 648 | if g.get(ARCH + '.attention.kv_source_layers') is not None: |
| 649 | if m: # already in the upstream layout; it may still lack the link to the Engram file |
| 650 | converted.append((os.path.dirname(os.path.abspath(path)), m.group('prefix'), g)) |
| 651 | continue |
| 652 | models.append(g) |
| 653 | elif arch == ARCH + '-engram' or TABLE in g.tensors: |
| 654 | engrams.setdefault(os.path.realpath(path), g) # links to the Engram file count once |
| 655 | elif g.get('clip.projector_type') == 'deepseek4v': |
| 656 | mmprojs.append(g) |
| 657 | elif m and g.get('split.no') is not None: |
| 658 | shards[(os.path.dirname(path), m.group('prefix'), int(m.group('no')))] = g |
| 659 | sets = [] |
| 660 | for g in models: |
| 661 | m = SPLIT_RE.match(os.path.basename(g.path)) |
| 662 | if m is None: |
| 663 | sets.append((g, [g], os.path.basename(g.path)[:-5])) |
| 664 | continue |
| 665 | prefix, count = m.group('prefix'), g.get('split.count') |
| 666 | parts = [g] + [shards.get((os.path.dirname(g.path), prefix, i)) for i in range(2, count + 1)] |
| 667 | if None in parts: |
| 668 | raise ValueError('%s: not all %d shards are present' % (g.path, count)) |
| 669 | sets.append((g, parts, prefix)) |
| 670 | orphans = sorted(g.path for (d, prefix, _), g in shards.items() |
| 671 | if (d, prefix) not in firsts and prefix.startswith(BUILD_PREFIX)) |
| 672 | if orphans: |
| 673 | raise ValueError('the first shard of these files is missing, so their build cannot be converted: %s' |
| 674 | % ', '.join(orphans)) |
| 675 | return sets, list(engrams.values()), mmprojs, converted |
| 676 | |
| 677 | |
| 678 | def repo_file(name, path, inputs, sha256, log): |
| 679 | """A small file of the repository with the given SHA-256: the given path, else a copy next to this script or in |
| 680 | the model folder (other versions, such as an older download, are skipped), else downloaded from the repository.""" |
| 681 | if path is not None: |
| 682 | candidates = [path] |
| 683 | else: |
| 684 | folders = [os.path.dirname(os.path.abspath(__file__))] |
| 685 | folders += [p if os.path.isdir(p) else os.path.dirname(os.path.abspath(p)) for p in inputs] |
| 686 | candidates = list(dict.fromkeys(f for f in (os.path.join(d, name) for d in folders) if os.path.isfile(f))) |
| 687 | for f in candidates: |
| 688 | with open(f, 'rb') as fin: |
| 689 | data = fin.read() |
| 690 | if hashlib.sha256(data).hexdigest() == sha256: |
| 691 | return data |
| 692 | if path is not None: |
| 693 | raise ValueError('%s is not the current %s of %s (checksum)' % (path, name, REPO)) |
| 694 | log('%s: not the current version (checksum), skipped' % f) |
| 695 | url = 'https://huggingface.co/%s/resolve/main/%s' % (REPO, name) |
| 696 | try: |
| 697 | with urllib.request.urlopen(url, timeout=120) as r: |
| 698 | data = r.read() |
| 699 | except OSError as e: |
| 700 | raise ValueError('%s could not be downloaded from %s (%s); put it next to this script' % (name, url, e)) |
| 701 | if hashlib.sha256(data).hexdigest() != sha256: |
| 702 | raise ValueError('%s of %s has changed since this script was written; download the current ' |
| 703 | 'convert_ds41_upstream.py' % (name, REPO)) |
| 704 | log('%s: downloaded from %s' % (name, REPO)) |
| 705 | return data |
| 706 | |
| 707 | |
| 708 | def read_inputs(args, log): |
| 709 | """chat_template.jinja (embedded in the new first shards) and exp_probs_b_vl.f32 (the vision router bias).""" |
| 710 | template = repo_file('chat_template.jinja', args.chat_template, args.inputs, TEMPLATE_SHA256, log).decode('utf-8') |
| 711 | bias_vl = repo_file('exp_probs_b_vl.f32', args.vision_bias, args.inputs, BIAS_VL_SHA256, log) |
| 712 | return template, bias_vl |
| 713 | |
| 714 | |
| 715 | def main(): |
| 716 | ap = argparse.ArgumentParser(description=__doc__.split('\n')[0]) |
| 717 | ap.add_argument('inputs', nargs='+', help='model folder(s) or GGUF files') |
| 718 | ap.add_argument('--out', help='folder for the new shards (default: next to the old ones)') |
| 719 | ap.add_argument('--engram', help='the Engram GGUF (default: the one found among the inputs)') |
| 720 | ap.add_argument('--chat-template', help='chat_template.jinja of the repository (default: next to this script or ' |
| 721 | 'in the model folder)') |
| 722 | ap.add_argument('--vision-bias', help='exp_probs_b_vl.f32 of the repository (default: as --chat-template)') |
| 723 | ap.add_argument('--metadata', help=argparse.SUPPRESS) # header of another first shard (release builds) |
| 724 | ap.add_argument('--name', help='output name prefix for a single unsplit input') |
| 725 | ap.add_argument('--no-verify', action='store_true', help='skip re-reading and hashing the written shards') |
| 726 | ap.add_argument('--delete-old', action='store_true', help='delete the old shards after a verified conversion') |
| 727 | ap.add_argument('--dry-run', action='store_true', help='only print what would be done') |
| 728 | args = ap.parse_args() |
| 729 | log = lambda s: print(s, flush=True) |
| 730 | if args.no_verify and not args.dry_run: |
| 731 | log('warning: --no-verify: the written files are not read back, nor checked against the published ones') |
| 732 | |
| 733 | sets, engrams, mmprojs, converted = find_sets(args.inputs) |
| 734 | if args.engram: |
| 735 | engrams = [GGUFFile(args.engram)] |
| 736 | if (sets or converted) and len(engrams) != 1: |
| 737 | raise SystemExit('expected one Engram file (%s), found %d; pass it with --engram' % (ENGRAM_FILE, len(engrams))) |
| 738 | if not sets and not mmprojs and not converted: |
| 739 | raise SystemExit('nothing to convert (no DeepSeek-V4.1 GSQ-RCO files found)') |
| 740 | engram = engrams[0] if engrams else None |
| 741 | chat_template = bias_vl = None |
| 742 | if sets: |
| 743 | chat_template, bias_vl = read_inputs(args, log) |
| 744 | written, old = [], [] |
| 745 | builds = [(d, prefix) for d, prefix, _ in converted] |
| 746 | n_tensors = {g.get('split.tensors.count') for _, _, g in converted} |
| 747 | for first, parts, prefix in sets: |
| 748 | meta = GGUFFile(args.metadata) if args.metadata else first |
| 749 | name = args.name or prefix |
| 750 | out_dir = os.path.abspath(args.out or os.path.dirname(os.path.abspath(first.path))) |
| 751 | log('%s -> %s' % (prefix, os.path.join(out_dir, name + '-*.gguf'))) |
| 752 | if args.dry_run: |
| 753 | _, tensors, splits = plan_model(meta, parts, engram, chat_template, bias_vl) |
| 754 | for i, part in enumerate(splits): |
| 755 | log(' shard %d: %d tensors, %.2f GB' % (i + 1, len(part), sum(t.nbytes for t in part) / 1e9)) |
| 756 | log(' shard %d: %s' % (N_SPLIT, os.path.basename(engram.path))) |
| 757 | n_tensors.add(len(tensors)) |
| 758 | continue |
| 759 | os.makedirs(out_dir, exist_ok=True) |
| 760 | paths, tensors = convert_model(meta, parts, engram, chat_template, bias_vl, name, out_dir, not args.no_verify, log) |
| 761 | written += paths |
| 762 | builds.append((out_dir, name)) |
| 763 | n_tensors.add(len(tensors)) |
| 764 | if args.delete_old: |
| 765 | for p in parts: |
| 766 | os.remove(p.path) |
| 767 | log(' deleted the %d old shards' % len(parts)) |
| 768 | else: |
| 769 | old += [p.path for p in parts] |
| 770 | |
| 771 | if engram is not None and (builds or sets): |
| 772 | if len(n_tensors) != 1: |
| 773 | raise ValueError('the builds differ in tensor count, so they cannot share one Engram file') |
| 774 | table, _ = engram_table(engram) |
| 775 | if args.dry_run: |
| 776 | if TABLE not in engram.tensors: |
| 777 | log('%s: header rewritten in place, the table data stay' % os.path.basename(engram.path)) |
| 778 | else: |
| 779 | convert_engram(engram, table, n_tensors.pop(), not args.no_verify, log) |
| 780 | for out_dir, name in builds: |
| 781 | link_engram(engram.path, name, out_dir, log) |
| 782 | for src in mmprojs: |
| 783 | out_dir = args.out or os.path.dirname(os.path.abspath(src.path)) |
| 784 | out_path = os.path.join(out_dir, os.path.basename(src.path)) |
| 785 | if args.dry_run: |
| 786 | log('%s: projector type deepseek4v -> deepseek41v' % src.path) |
| 787 | continue |
| 788 | if os.path.abspath(out_path) == os.path.abspath(src.path): |
| 789 | convert_mmproj(src, out_path + '.new', os.path.basename(out_path), log) |
| 790 | os.replace(out_path + '.new', out_path) |
| 791 | else: |
| 792 | convert_mmproj(src, out_path, os.path.basename(out_path), log) |
| 793 | if args.dry_run: |
| 794 | log('dry run: only the headers were read; the tensor data are read and checked during the conversion') |
| 795 | elif old: |
| 796 | log('done. llama.cpp no longer needs the old shards:\n ' + '\n '.join(old)) |
| 797 | else: |
| 798 | log('done.') |
| 799 | |
| 800 | |
| 801 | # exp_probs_b_vl.f32: blk.0..39.exp_probs_b_vl = layers.N.ffn.gate.bias_vl of deepseek-ai/DeepSeek-V4.1-Flash-1017 |
| 802 | # (revision 1bae0e1088993b9b82de08f59de5be5a34e42170), 40 x 384 little-endian F32 |
| 803 | BIAS_VL_SHA256 = '710b11267b4db392d1fdb558f36ba8e0e91a91f4e8180c953c912e18eacf843d' |
| 804 | # chat_template.jinja of the repository since October 25, 2026 (commit 1c100b9), as in the published first shards |
| 805 | TEMPLATE_SHA256 = '4d61d22c90ea0cdf15c7e298f7215f23eeffd20f8eed8fb517fda5fb64693d99' |
| 806 | # SHA-256 of the files this script writes, as published in the repository since October 27, 2026 (commit a419d74). |
| 807 | # Every published version of the old files converts to these. |
| 808 | PUBLISHED = { |
| 809 | 'DeepSeek-V4.1-Flash-1017-GSQ-RCO-IQ1_M-00001-of-00006.gguf': |
| 810 | '3e8ddd11441edd4bc19bacaef5dbe38818af2cbdf0820fb70f1d2d851fcc192a', |
| 811 | 'DeepSeek-V4.1-Flash-1017-GSQ-RCO-IQ1_M-00002-of-00006.gguf': |
| 812 | 'be4ceb1caf1bed28e886860cf6a52adc8f5ca9eb0d2098ade39bb4179d48b323', |
| 813 | 'DeepSeek-V4.1-Flash-1017-GSQ-RCO-IQ1_M-00003-of-00006.gguf': |
| 814 | '758c66ba65236e8ce0820c21a7b6f9b2947681df8cc509f672a77722e8191b9a', |
| 815 | 'DeepSeek-V4.1-Flash-1017-GSQ-RCO-IQ1_M-00004-of-00006.gguf': |
| 816 | '04a5bea2b38eb576f49efc19c0a677768d6e2a8ea5772d72fb6cb79e1733a212', |
| 817 | 'DeepSeek-V4.1-Flash-1017-GSQ-RCO-IQ1_M-00005-of-00006.gguf': |
| 818 | '7c8dbee99890f05dbc420eba6f44393f5228d06248fe61e1ae8ed002449c7549', |
| 819 | 'DeepSeek-V4.1-Flash-1017-GSQ-RCO-IQ2_XS-00001-of-00006.gguf': |
| 820 | '0182a2a1f42d319a0dbc6e34aa82a063880c069fe3aafbbea86018f386e64e5f', |
| 821 | 'DeepSeek-V4.1-Flash-1017-GSQ-RCO-IQ2_XS-00002-of-00006.gguf': |
| 822 | '5c88039fa640460a9fc3465a6dfcd162a4083dd738d55e5b52e025be14555a08', |
| 823 | 'DeepSeek-V4.1-Flash-1017-GSQ-RCO-IQ2_XS-00003-of-00006.gguf': |
| 824 | '0fddebfd05f0a36cfd107ba5dfd2547e64df61adbab39560a5a5f5521ca099a5', |
| 825 | 'DeepSeek-V4.1-Flash-1017-GSQ-RCO-IQ2_XS-00004-of-00006.gguf': |
| 826 | 'c577a43c6e6e615838a1655a0b162541edeac6f36542f08c4a9593a70dcaf500', |
| 827 | 'DeepSeek-V4.1-Flash-1017-GSQ-RCO-IQ2_XS-00005-of-00006.gguf': |
| 828 | '851d8605bf504962054d17ea8a0d9e2e0a228d085d396bd865a4de9f16d90fbd', |
| 829 | 'DeepSeek-V4.1-Flash-1017-GSQ-RCO-IQ2_XXS-00001-of-00006.gguf': |
| 830 | 'f356f198561f56df36f7ab92d7238a926646d76de992c34f15bcb939a5f529b9', |
| 831 | 'DeepSeek-V4.1-Flash-1017-GSQ-RCO-IQ2_XXS-00002-of-00006.gguf': |
| 832 | '25af3785ccf925f54137e1ead4c481a8952315dc0b1af6519a99407f67bce8f3', |
| 833 | 'DeepSeek-V4.1-Flash-1017-GSQ-RCO-IQ2_XXS-00003-of-00006.gguf': |
| 834 | '8168d0998fc494def2e1952d83c131425a108660aae9d29190f5a8670ab62b48', |
| 835 | 'DeepSeek-V4.1-Flash-1017-GSQ-RCO-IQ2_XXS-00004-of-00006.gguf': |
| 836 | '9eb9b4de1968f150dfc207eb31c27be9cd9b5666a9a6246bde8a22b184b4bee0', |
| 837 | 'DeepSeek-V4.1-Flash-1017-GSQ-RCO-IQ2_XXS-00005-of-00006.gguf': |
| 838 | '7e35f6b4e6fc46f6c4b152b593d84d683c5ece695ae6ad734f556198c041bec1', |
| 839 | 'DeepSeek-V4.1-Flash-Engram-IQ4_NL.gguf': |
| 840 | '3dac4c54893ad7aa89103c0028500eadc78e74112cef3a2930a97e67dc9fb4f3', |
| 841 | 'mmproj-DeepSeek-V4.1-Flash-1017-BF16.gguf': |
| 842 | '0fdc4b7693f287e9665940cf42bd39c546ffe4f0def89c569d7d0e45d60d2df1', |
| 843 | } |
| 844 | |
| 845 | if __name__ == '__main__': |
| 846 | try: |
| 847 | main() |
| 848 | except (ValueError, OSError) as e: |
| 849 | sys.exit('error: %s' % e) |
| 850 | |