Spaces:
Running
Running
Download js/engine/sample.js from UtopiaIA/elffuss-code: direct link, hf CLI and curl.
- Browser
- Download file 8.94 kB
-
https://huggingface.co/spaces/UtopiaIA/elffuss-code/resolve/main/js/engine/sample.js
- Command line
-
hf download hf://spaces/UtopiaIA/elffuss-code/js/engine/sample.js
-
curl -L -o sample.js https://huggingface.co/spaces/UtopiaIA/elffuss-code/resolve/main/js/engine/sample.js
8.94 kB
| // SPDX-License-Identifier: Apache-2.0 | |
| // Copyright (c) 2026 UtopiaIA | |
| // Elffuss Runtime · muestreo y bucle de generación. | |
| // ───────────────────────────────────────────────────────────────────────────── | |
| // El bucle de decode es donde el motor pasa el 99 % del tiempo, y donde falla lo | |
| // que un forward suelto no puede detectar: que el estado se arrastre bien entre | |
| // llamadas. Un forward aislado empieza con el estado a cero y la caché vacía, así | |
| // que valida la matemática pero no la memoria. | |
| // | |
| // La prueba que sí lo detecta es de consistencia interna: procesar «abc» de una | |
| // vez tiene que dar el MISMO resultado que procesar «a», luego «b», luego «c». | |
| // Si la caché KV se escribe en el sitio equivocado, si el estado recurrente no | |
| // se arrastra o si las posiciones de RoPE se reinician, las dos formas divergen. | |
| // | |
| // Licencia: código propio, Apache-2.0 (ver SPDX arriba y LICENSE). Sin dependencias externas. | |
| // Muestreo sobre los logits ya en CPU. Se trabaja sobre el último token, que es | |
| // el único que interesa al generar. Sigue el ORDEN de llama.cpp, que es la | |
| // referencia con la que se mide todo: penalizaciones → top-k → top-p → min-p → | |
| // temperatura. La temperatura va AL FINAL: top-p y min-p se calculan sobre la | |
| // distribución sin temperatura y la temperatura solo reparte entre los que | |
| // sobreviven. Antes iba al principio, y con temperatura < 1 (la ficha del 27B | |
| // pide 0,7 con top-p 0,8) el top-p cortaba sobre una distribución más | |
| // afilada: sobrevivían menos tokens que en llama.cpp, sin error ninguno. | |
| // · temp = 0 → greedy, determinista (tras las penalizaciones). | |
| // · repeatPenalty: la de llama.cpp (divide si el logit es positivo y | |
| // multiplica si es negativo). Su defecto aquí sigue siendo 1,1 para no | |
| // cambiar a los modelos que no declaran muestreo; el llama.cpp actual trae | |
| // 1,0, y los modelos con ficha la apagan desde el registro. | |
| // · presence: resta fija a todo token que aparezca en la ventana (la ficha | |
| // del 27B pide 1,5 sin razonar), como `presence_penalty` de llama.cpp. | |
| // · minP: descarta los tokens cuya probabilidad sea menor que minP × la del | |
| // más probable. | |
| // mulberry32 sobre la semilla: buena mezcla también con semillas consecutivas | |
| // (seed + paso), que es como se llama token a token. | |
| function uniforme(seed) { | |
| let t = ((seed >>> 0) + 0x6D2B79F5) >>> 0; | |
| t = Math.imul(t ^ (t >>> 15), t | 1); | |
| t ^= t + Math.imul(t ^ (t >>> 7), t | 61); | |
| return ((t ^ (t >>> 14)) >>> 0) / 4294967296; | |
| } | |
| // ¿Va `a` antes que `b`? Logit mayor primero; a igualdad, índice menor. | |
| const antes = (logits, a, b) => logits[a] > logits[b] || (logits[a] === logits[b] && a < b); | |
| // Los K mejores índices, ordenados. Montículo de MÍNIMOS de tamaño K (la raíz es | |
| // el peor de los que se guardan): cada logit nuevo solo entra si gana a la raíz. | |
| export function topKIndices(logits, k) { | |
| const h = new Int32Array(k); | |
| let size = 0; | |
| const peor = (a, b) => antes(logits, b, a); // a es «peor» que b | |
| const sube = (i) => { while (i > 0) { const p = (i - 1) >> 1; if (!peor(h[i], h[p])) break; [h[i], h[p]] = [h[p], h[i]]; i = p; } }; | |
| const baja = (i) => { | |
| for (;;) { | |
| const l = 2 * i + 1, r = l + 1; let m = i; | |
| if (l < size && peor(h[l], h[m])) m = l; | |
| if (r < size && peor(h[r], h[m])) m = r; | |
| if (m === i) return; | |
| [h[i], h[m]] = [h[m], h[i]]; i = m; | |
| } | |
| }; | |
| for (let i = 0; i < logits.length; i++) { | |
| if (size < k) { h[size] = i; sube(size++); } | |
| else if (antes(logits, i, h[0])) { h[0] = i; baja(0); } | |
| } | |
| return Array.from(h.subarray(0, size)).sort((a, b) => (antes(logits, a, b) ? -1 : 1)); | |
| } | |
| // Sin top-k: los índices a menos de `margen` del máximo, ordenados. | |
| function candidatos(logits, margen) { | |
| let max = -Infinity; | |
| for (let i = 0; i < logits.length; i++) if (logits[i] > max) max = logits[i]; | |
| const out = []; | |
| for (let i = 0; i < logits.length; i++) if (logits[i] >= max - margen) out.push(i); | |
| return out.sort((a, b) => (antes(logits, a, b) ? -1 : 1)); | |
| } | |
| export function sample(logits, opts = {}) { | |
| const { temperature = 0, topP = 1, topK = 0, minP = 0, seed = 1, | |
| repeatPenalty = 1.1, repeatLastN = 64, presence = 0, previos = null } = opts; | |
| const n = logits.length; | |
| // PENALIZACIONES, sobre los últimos `repeatLastN` (prompt incluido, como | |
| // llama.cpp). La de repetición existe porque el decode voraz sobre un modelo | |
| // muy cuantizado entra en bucle («el color es de color, el color es de…»). | |
| if (previos && previos.length && (repeatPenalty !== 1 || presence)) { | |
| const desde = Math.max(0, previos.length - repeatLastN); | |
| const vistos = new Set(); | |
| for (let i = desde; i < previos.length; i++) vistos.add(previos[i]); | |
| for (const id of vistos) { | |
| if (id < 0 || id >= n) continue; | |
| if (repeatPenalty !== 1) logits[id] = logits[id] > 0 ? logits[id] / repeatPenalty : logits[id] * repeatPenalty; | |
| if (presence) logits[id] -= presence; | |
| } | |
| } | |
| if (temperature <= 0) { | |
| let best = 0; | |
| for (let i = 1; i < n; i++) if (logits[i] > logits[best]) best = i; | |
| return best; | |
| } | |
| // Los candidatos ordenados por logit (desc; a igual logit, el índice menor | |
| // primero, que es lo que daba la ordenación estable de antes). NO se ordena el | |
| // vocabulario entero: eso eran decenas de milisegundos POR TOKEN en JS, más | |
| // que el forward de un modelo pequeño. Con top-k se seleccionan los K con un | |
| // montículo en una pasada; sin top-k, solo los que quedan a menos de 40 nats | |
| // del máximo (fuera de ahí la probabilidad no llega a 1e-17 cada uno). | |
| const idx = topK > 0 ? topKIndices(logits, Math.min(topK, n)) : candidatos(logits, 40); | |
| const keep = idx.length; | |
| // top-p y min-p SIN temperatura. Resta del máximo antes de exp(): sin ella, | |
| // logits de 30 desbordan. | |
| const max = logits[idx[0]]; | |
| const base = new Float64Array(keep); | |
| let total = 0; | |
| for (let i = 0; i < keep; i++) { base[i] = Math.exp(logits[idx[i]] - max); total += base[i]; } | |
| let corte = keep; | |
| if (topP < 1) { | |
| let acc = 0; | |
| for (let i = 0; i < keep; i++) { | |
| acc += base[i] / total; | |
| if (acc >= topP) { corte = i + 1; break; } | |
| } | |
| } | |
| if (minP > 0) { | |
| // base[0] es el más probable (están ordenados): el umbral es relativo a él. | |
| const umbral = minP * base[0]; | |
| let k = 1; | |
| while (k < corte && base[k] >= umbral) k++; | |
| corte = k; | |
| } | |
| // La temperatura, solo entre los supervivientes. | |
| const probs = new Float64Array(corte); | |
| let sub = 0; | |
| for (let i = 0; i < corte; i++) { probs[i] = Math.exp((logits[idx[i]] - max) / temperature); sub += probs[i]; } | |
| // Generador propio y con semilla: Math.random() haría irreproducible | |
| // cualquier prueba de regresión de generación. Uniforme en [0, 1). | |
| // | |
| // ⚠️ Antes era un paso de LCG con `(s >>> 8) / 8388608`: el numerador llega a | |
| // 2²⁴ y el divisor es 2²³, así que el «uniforme» caía en [0, 2). La mitad de | |
| // las veces r > sub, el bucle de abajo se salía sin elegir y devolvía el | |
| // ÚLTIMO superviviente: el menos probable (sin top-k, el peor token de todo el | |
| // vocabulario). Y semillas consecutivas daban valores correlados. Lo cazó | |
| // una prueba al pedir que un token improbable saliera alguna vez. | |
| let r = uniforme(seed) * sub; | |
| for (let i = 0; i < corte; i++) { | |
| r -= probs[i]; | |
| if (r <= 0) return idx[i]; | |
| } | |
| return idx[corte - 1]; | |
| } | |
| // Genera tokens uno a uno. El prompt entero va en UNA llamada (prefill) y cada | |
| // token nuevo en la suya (decode), que es exactamente el patrón real. | |
| export async function generate(session, readLogits, promptIds, opts = {}) { | |
| const max = opts.maxTokens || 32; | |
| const stop = new Set(opts.stopIds || []); | |
| const onToken = opts.onToken || (() => {}); | |
| const out = []; | |
| let ids = promptIds; | |
| for (let step = 0; step < max; step++) { | |
| const { logits, logitsTokens } = await session.forward(ids); | |
| // El forward devuelve logits SOLO del último token salvo que se pidan todos: | |
| // deducir el vocabulario dividiendo por el número de tokens del prompt daría | |
| // un vocabulario 316 veces más pequeño y una lectura corrida. | |
| const vocab = logits.size / 4 / logitsTokens; | |
| const all = await readLogits(logits, logitsTokens * vocab); | |
| const last = all.subarray((logitsTokens - 1) * vocab, logitsTokens * vocab); | |
| // Se le pasan los tokens ya emitidos MÁS el prompt: repetir lo que acaba de | |
| // decir el usuario es tan bucle como repetirse a sí mismo. | |
| const id = sample(last, { ...opts, seed: (opts.seed || 1) + step, | |
| previos: promptIds.concat(out) }); | |
| session.recycle(); | |
| if (stop.has(id)) break; | |
| out.push(id); | |
| onToken(id, step); | |
| ids = [id]; | |
| } | |
| return out; | |
| } | |