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;
}