Spaces:
Running
Running
File size: 8,943 Bytes
4bd8362 | 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 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | // 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;
}
|