File size: 8,006 Bytes
30bafb7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | // config.json -> a normalized model spec, and the tensor names to fetch.
//
// "Load any model off HuggingFace" is not a file-format problem, it is an
// architecture problem: the weights are easy to read, but a Llama block and a
// GPT-2 block are different computations. This file is where that difference
// is confined. Everything downstream — sharding, the ring, the kernels —
// works off the normalized spec and never asks what family a model came from.
//
// Supported families, and what each one needs from the forward pass:
//
// llama / mistral / qwen2 / smollm / tinyllama RMSNorm, RoPE, GQA, SwiGLU
// gpt2 / gpt2-style LayerNorm+bias, learned
// positions, fused QKV, GELU
//
// Anything else is refused BY NAME with a clear message. Silently treating an
// unknown architecture as a known one would produce fluent, confident, wrong
// output — the exact failure this project spends its verification budget on
// making impossible.
(function (root) {
"use strict";
const LLAMA_LIKE = new Set(["llama", "mistral", "qwen2", "qwen3", "smollm", "tinyllama", "olmo", "phi3"]);
const GPT2_LIKE = new Set(["gpt2", "gpt_neo"]);
function pick(cfg, keys, dflt) {
for (const k of keys) if (cfg[k] != null) return cfg[k];
return dflt;
}
function fromConfig(cfg) {
const type = String(cfg.model_type || "").toLowerCase();
const family = LLAMA_LIKE.has(type) ? "llama" : GPT2_LIKE.has(type) ? "gpt2" : null;
if (!family)
throw new Error(`unsupported architecture "${cfg.model_type || "unknown"}". ` +
`This build implements Llama-style (RMSNorm/RoPE/GQA/SwiGLU) and GPT-2-style blocks. ` +
`Running a different block shape through these kernels would produce confident nonsense, so it is refused.`);
const hidden = pick(cfg, ["hidden_size", "n_embd"], 0);
const layers = pick(cfg, ["num_hidden_layers", "n_layer"], 0);
const heads = pick(cfg, ["num_attention_heads", "n_head"], 0);
const vocab = pick(cfg, ["vocab_size"], 0);
if (!hidden || !layers || !heads || !vocab)
throw new Error("config.json is missing hidden_size / num_hidden_layers / num_attention_heads / vocab_size");
const kvHeads = family === "llama" ? pick(cfg, ["num_key_value_heads"], heads) : heads;
const headDim = pick(cfg, ["head_dim"], Math.floor(hidden / heads));
if (heads * headDim !== hidden && family === "gpt2")
throw new Error(`head geometry does not divide the hidden size (${heads} heads x ${headDim} != ${hidden})`);
const spec = {
family, modelType: type, hidden, layers, heads, kvHeads, headDim, vocab,
inter: pick(cfg, ["intermediate_size", "n_inner"], family === "gpt2" ? 4 * hidden : 4 * hidden),
maxPos: pick(cfg, ["max_position_embeddings", "n_positions", "n_ctx"], 2048),
normEps: pick(cfg, ["rms_norm_eps", "layer_norm_epsilon", "layer_norm_eps"], 1e-5),
norm: family === "llama" ? "rms" : "ln",
act: family === "llama" ? "silu" : "gelu",
gated: family === "llama", // SwiGLU has a gate branch; GELU MLP does not
rope: family === "llama",
ropeTheta: pick(cfg, ["rope_theta"], 10000),
tie: pick(cfg, ["tie_word_embeddings"], family === "gpt2"),
qkvFused: family === "gpt2", // GPT-2 packs q,k,v in one c_attn
bias: family === "gpt2", // Llama-likes are bias-free
// GPT-2's Conv1D stores (in, out) — already the k x n our GEMM wants.
// torch.nn.Linear stores (out, in) and must be transposed at load.
weightLayout: family === "gpt2" ? "in_out" : "out_in",
};
if (spec.gated && !cfg.intermediate_size)
throw new Error("a gated MLP needs intermediate_size in config.json");
if (spec.kvHeads && spec.heads % spec.kvHeads !== 0)
throw new Error(`num_attention_heads (${spec.heads}) is not a multiple of num_key_value_heads (${spec.kvHeads})`);
return spec;
}
// ---- tensor names -----------------------------------------------------------
// Returned as { role: name }. Roles are what the forward pass asks for, so a
// new family only has to be named here, never plumbed through.
function layerTensors(spec, i) {
if (spec.family === "llama") {
const p = `model.layers.${i}.`;
// The q/k/v biases are listed but OPTIONAL, and that is not a detail:
// Qwen2.5 ships them while SmolLM does not, and neither says so in
// config.json (`attention_bias` is simply absent from Qwen's). Trusting
// the config would silently drop three bias vectors per layer — a model
// that still generates fluent text, just not the right text. So bias is
// decided by what the weight file actually contains.
return {
nrm1: p + "input_layernorm.weight",
nrm2: p + "post_attention_layernorm.weight",
Wq: p + "self_attn.q_proj.weight", bq: p + "self_attn.q_proj.bias",
Wk: p + "self_attn.k_proj.weight", bk: p + "self_attn.k_proj.bias",
Wv: p + "self_attn.v_proj.weight", bv: p + "self_attn.v_proj.bias",
Wo: p + "self_attn.o_proj.weight", bo: p + "self_attn.o_proj.bias",
Wgate: p + "mlp.gate_proj.weight", Wup: p + "mlp.up_proj.weight", Wdown: p + "mlp.down_proj.weight",
};
}
const p = `h.${i}.`; // gpt2 (also seen as transformer.h.N)
return {
nrm1: p + "ln_1.weight", nrm1b: p + "ln_1.bias",
nrm2: p + "ln_2.weight", nrm2b: p + "ln_2.bias",
Wqkv: p + "attn.c_attn.weight", bqkv: p + "attn.c_attn.bias",
Wo: p + "attn.c_proj.weight", bo: p + "attn.c_proj.bias",
Wfc: p + "mlp.c_fc.weight", bfc: p + "mlp.c_fc.bias",
Wdown: p + "mlp.c_proj.weight", bdown: p + "mlp.c_proj.bias",
};
}
function headTensors(spec) {
if (spec.family === "llama")
return { emb: "model.embed_tokens.weight", nrmF: "model.norm.weight",
lmHead: spec.tie ? null : "lm_head.weight" };
return { emb: "wte.weight", pos: "wpe.weight",
nrmF: "ln_f.weight", nrmFb: "ln_f.bias",
lmHead: spec.tie ? null : "lm_head.weight" };
}
// Repos differ on whether GPT-2 tensors carry a "transformer." prefix, and
// some Llama exports drop "model.". Rather than guessing, resolve each name
// against the tensors the file actually contains.
function resolver(available) {
const has = (n) => available.has(n);
return function resolve(name, optional) {
if (name == null) return null;
if (has(name)) return name;
for (const alt of ["transformer." + name, name.replace(/^model\./, ""), "model." + name,
name.replace(/^transformer\./, "")])
if (has(alt)) return alt;
if (optional) return null;
throw new Error(`the weights do not contain "${name}" — this repo's layout is not one this build recognises`);
};
}
// Every tensor one stage needs: its layers, plus the head's pieces if it is
// the head. This is the list that becomes byte ranges.
function tensorsFor(spec, available, st) {
const resolve = resolver(available);
const want = [];
const add = (n, opt) => { const r = resolve(n, opt); if (r) want.push(r); };
if (st.head) {
const h = headTensors(spec);
add(h.emb); add(h.pos, true); add(h.nrmF, true); add(h.nrmFb, true); add(h.lmHead, true);
}
for (let l = st.lo; l < st.hi; l++) {
const t = layerTensors(spec, l);
// bias roles (b*) are optional everywhere: present in the file or not
// applied at all. Weight roles (W*, nrm*) are required.
for (const [role, n] of Object.entries(t)) add(n, /^b/.test(role));
}
return want;
}
const api = { fromConfig, layerTensors, headTensors, tensorsFor, resolver, LLAMA_LIKE, GPT2_LIKE };
if (typeof module !== "undefined" && module.exports) module.exports = api;
else root.Arch = api;
})(typeof self !== "undefined" ? self : this);
|