Download sample.mjs from LUSCAINK/SEPIA-0: direct link, hf CLI and curl.
- Browser
- Download file 9.13 kB
-
https://huggingface.co/LUSCAINK/SEPIA-0/resolve/main/sample.mjs
- Command line
-
hf download hf://LUSCAINK/SEPIA-0/sample.mjs
-
curl -L -o sample.mjs https://huggingface.co/LUSCAINK/SEPIA-0/resolve/main/sample.mjs
9.13 kB
| // SEPIA-0 inference in Node.js, no dependencies. | |
| // | |
| // node sample.mjs "The validator" --n 240 --temperature 0.8 --seed 7 | |
| // | |
| // Same arithmetic and sampling as the LUSCA server (shared/sepia/model.mjs and | |
| // POST /api/generate): float32 weights and activations, prompt CRLF/CR β LF and | |
| // cut to 200 characters, n clamped to 1..600 (default 240), temperature clamped | |
| // to 0.05..2 (default 0.8), the last 16 prompt symbols (left-padded with | |
| // newline) as context. With --seed the generator is mulberry32, so | |
| // `python inference.py` prints the same text for the same seed; without it, | |
| // Math.random is used, as on the server. | |
| // | |
| // Also usable as a module: | |
| // import { loadSepia, generate, logits, mulberry32 } from './sample.mjs' | |
| import { readFileSync } from 'node:fs' | |
| import { resolve } from 'node:path' | |
| import { pathToFileURL } from 'node:url' | |
| /** id 0 = '\n', ids 1..95 = printable ASCII 32..126 (' ' .. '~'). */ | |
| export const VOCAB = '\n' + Array.from({ length: 95 }, (_, i) => String.fromCharCode(32 + i)).join('') | |
| export const NL = 0 | |
| export const SP = 1 | |
| // βββ weights ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * Minimal safetensors reader (F32 tensors). | |
| * @param {string | URL} file | |
| * @returns {{ tensors: Record<string, { shape: number[], data: Float32Array }>, metadata: Record<string, string> }} | |
| */ | |
| export function readSafetensors(file) { | |
| const buf = readFileSync(file) | |
| const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength) | |
| const n = Number(dv.getBigUint64(0, true)) | |
| const header = JSON.parse(buf.toString('utf8', 8, 8 + n)) | |
| const metadata = header.__metadata__ ?? {} | |
| delete header.__metadata__ | |
| /** @type {Record<string, { shape: number[], data: Float32Array }>} */ | |
| const tensors = {} | |
| for (const [name, info] of Object.entries(header)) { | |
| if (info.dtype !== 'F32') throw new Error(`${name}: dtype ${info.dtype} is not supported`) | |
| const [begin, end] = info.data_offsets | |
| const data = new Float32Array((end - begin) / 4) | |
| for (let i = 0; i < data.length; i++) data[i] = dv.getFloat32(8 + n + begin + i * 4, true) | |
| tensors[name] = { shape: info.shape, data } | |
| } | |
| return { tensors, metadata } | |
| } | |
| /** | |
| * logits = tanh(concat_t emb[ids_t] @ W1 + b1) @ W2 + b2 (matrices are [in, out]) | |
| * @param {string | URL} [file] defaults to model.safetensors next to this script | |
| */ | |
| export function loadSepia(file = new URL('./model.safetensors', import.meta.url)) { | |
| const { tensors: t, metadata } = readSafetensors(file) | |
| const [V, E] = t.emb.shape | |
| const H = t.W1.shape[1] | |
| const T = t.W1.shape[0] / E | |
| return { | |
| T, E, H, V, metadata, | |
| emb: t.emb.data, W1: t.W1.data, b1: t.b1.data, W2: t.W2.data, b2: t.b2.data, | |
| xe: new Float32Array(T * E), h: new Float32Array(H), lg: new Float32Array(V), | |
| } | |
| } | |
| /** | |
| * Logits for one context of T ids (oldest first). Returns a view overwritten by the next call. | |
| * @param {ReturnType<typeof loadSepia>} m | |
| * @param {ArrayLike<number>} ctx | |
| */ | |
| export function logits(m, ctx) { | |
| const { T, E, H, V, emb, W1, b1, W2, b2, xe, h, lg } = m | |
| for (let t = 0; t < T; t++) xe.set(emb.subarray(ctx[t] * E, ctx[t] * E + E), t * E) | |
| h.set(b1) | |
| for (let i = 0; i < T * E; i++) { | |
| const a = xe[i] | |
| const wo = i * H | |
| for (let j = 0; j < H; j++) h[j] += a * W1[wo + j] | |
| } | |
| for (let j = 0; j < H; j++) h[j] = Math.tanh(h[j]) | |
| lg.set(b2) | |
| for (let i = 0; i < H; i++) { | |
| const a = h[i] | |
| const wo = i * V | |
| for (let j = 0; j < V; j++) lg[j] += a * W2[wo + j] | |
| } | |
| return lg | |
| } | |
| // βββ vocabulary (same mapping as shared/sepia/model.mjs encode) βββββββββββββ | |
| const DROP = -1 | |
| const MAP = new Int8Array(65536).fill(SP) // default: unknown symbol β space | |
| for (let c = 32; c <= 126; c++) MAP[c] = c - 31 | |
| for (let c = 0; c < 32; c++) MAP[c] = DROP // C0 controls | |
| MAP[10] = NL | |
| MAP[9] = SP | |
| MAP[11] = NL | |
| MAP[12] = NL | |
| for (let c = 127; c <= 159; c++) MAP[c] = DROP // DEL + C1 controls | |
| MAP[0x85] = NL | |
| MAP[0x2028] = NL | |
| MAP[0x2029] = NL | |
| /** @param {string} chars @param {number} id */ | |
| const setAll = (chars, id) => { | |
| for (let i = 0; i < chars.length; i++) MAP[chars.charCodeAt(i)] = id | |
| } | |
| /** @param {number} a @param {number} b @param {number} id */ | |
| const setRange = (a, b, id) => { | |
| for (let c = a; c <= b; c++) MAP[c] = id | |
| } | |
| setAll('βββββ²β΅ΚΌΚΉΒ΄οΌ', "'".charCodeAt(0) - 31) | |
| setAll('βββββ³βΆΒ«Β»ΚΊοΌ', '"'.charCodeAt(0) - 31) | |
| setAll('ββββββββοΉοΉ£οΌβΈΊβΈ»', '-'.charCodeAt(0) - 31) | |
| setAll('β’β£ββͺβΒ·', '*'.charCodeAt(0) - 31) | |
| setAll('Γ', 'x'.charCodeAt(0) - 31) | |
| setAll('Βο»Ώ', DROP) // soft hyphen, BOM | |
| setRange(0x200b, 0x200f, DROP) // zero-width chars, LRM/RLM | |
| setRange(0x2060, 0x2064, DROP) | |
| setRange(0x0300, 0x036f, DROP) // combining diacritics (after NFKD) | |
| setRange(0x1ab0, 0x1aff, DROP) | |
| setRange(0x1dc0, 0x1dff, DROP) | |
| setRange(0x20d0, 0x20ff, DROP) | |
| setRange(0xfe00, 0xfe0f, DROP) // variation selectors | |
| setRange(0xfe20, 0xfe2f, DROP) | |
| setRange(0xdc00, 0xdfff, DROP) // low surrogates: a character beyond the BMP becomes ONE space | |
| /** | |
| * Text β vocabulary ids (NFKD, symbol folding, whitespace tidying as in the LUSCA corpus). | |
| * @param {string} text | |
| * @param {boolean} [keepTrailing] keep trailing spaces/newlines (prompts) | |
| * @returns {Uint8Array} | |
| */ | |
| export function encode(text, keepTrailing = false) { | |
| let s = typeof text === 'string' ? text : String(text ?? '') | |
| try { | |
| s = s.normalize('NFKD') | |
| } catch { | |
| /* lone surrogates etc. β fall back to the raw string */ | |
| } | |
| const out = new Uint8Array(s.length) | |
| let n = 0 | |
| for (let i = 0; i < s.length; i++) { | |
| const id = MAP[s.charCodeAt(i)] | |
| if (id === DROP) continue | |
| if (id === SP) { | |
| if (n === 0 || out[n - 1] === SP || out[n - 1] === NL) continue | |
| } else if (id === NL) { | |
| if (n > 0 && out[n - 1] === SP) n-- | |
| if (n === 0) continue | |
| if (n >= 2 && out[n - 1] === NL && out[n - 2] === NL) continue | |
| } | |
| out[n++] = id | |
| } | |
| if (!keepTrailing) while (n > 0 && (out[n - 1] === SP || out[n - 1] === NL)) n-- | |
| return out.slice(0, n) | |
| } | |
| /** @param {ArrayLike<number>} ids */ | |
| export function decode(ids) { | |
| let s = '' | |
| for (let i = 0; i < ids.length; i++) s += VOCAB[ids[i]] ?? ' ' | |
| return s | |
| } | |
| // βββ sampling βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** Seedable PRNG (mulberry32), floats in [0, 1). @param {number} seed */ | |
| export function mulberry32(seed) { | |
| let a = seed >>> 0 | |
| return () => { | |
| a = (a + 0x6d2b79f5) >>> 0 | |
| let t = a | |
| t = Math.imul(t ^ (t >>> 15), t | 1) | |
| t ^= t + Math.imul(t ^ (t >>> 7), t | 61) | |
| return ((t ^ (t >>> 14)) >>> 0) / 4294967296 | |
| } | |
| } | |
| /** | |
| * Prompt + n sampled characters, with /api/generate's input handling. | |
| * @param {ReturnType<typeof loadSepia>} m | |
| * @param {string} prompt | |
| * @param {{ n?: number, temperature?: number, rand?: () => number }} [o] | |
| */ | |
| export function generate(m, prompt = '', o = {}) { | |
| const p0 = String(prompt ?? '').replace(/\r\n?/g, '\n').slice(0, 200) | |
| const n = Math.min(600, Math.max(1, Math.round(Number(o.n ?? 240)))) | |
| const temperature = Math.min(2, Math.max(0.05, Number(o.temperature ?? 0.8))) | |
| const rand = o.rand ?? Math.random | |
| const { T, V } = m | |
| const p = encode(p0, true) | |
| const ctx = new Uint8Array(T).fill(NL) | |
| const k = Math.min(T, p.length) | |
| ctx.set(p.subarray(p.length - k), T - k) | |
| const out = new Uint8Array(n) | |
| const temp = Math.max(1e-3, temperature) | |
| const probs = new Float64Array(V) | |
| for (let i = 0; i < n; i++) { | |
| const lg = logits(m, ctx) | |
| let max = -Infinity | |
| for (let c = 0; c < V; c++) if (lg[c] > max) max = lg[c] | |
| let sum = 0 | |
| for (let c = 0; c < V; c++) sum += probs[c] = Math.exp((lg[c] - max) / temp) | |
| let r = rand() * sum | |
| let id = V - 1 | |
| for (let c = 0; c < V; c++) { | |
| r -= probs[c] | |
| if (r <= 0) { | |
| id = c | |
| break | |
| } | |
| } | |
| out[i] = id | |
| ctx.copyWithin(0, 1) | |
| ctx[T - 1] = id | |
| } | |
| return p0 + decode(out) | |
| } | |
| function main() { | |
| const args = process.argv.slice(2) | |
| const opt = (name, def) => { | |
| const i = args.indexOf(`--${name}`) | |
| if (i < 0) return def | |
| const v = args[i + 1] | |
| args.splice(i, 2) | |
| return v | |
| } | |
| if (args.includes('--help') || args.includes('-h')) { | |
| console.log('usage: node sample.mjs [prompt] [--n 240] [--temperature 0.8] [--seed S] [--weights model.safetensors]') | |
| return | |
| } | |
| const n = Number(opt('n', 240)) | |
| const temperature = Number(opt('temperature', 0.8)) | |
| const seed = opt('seed', null) | |
| const weights = opt('weights', null) | |
| const m = loadSepia(weights ? resolve(weights) : undefined) | |
| const rand = seed !== null ? mulberry32(Number(seed)) : Math.random | |
| process.stdout.write(generate(m, args[0] ?? '', { n, temperature, rand }) + '\n') | |
| } | |
| if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) main() | |