import { numpy as np, nn, jit, valueAndGrad, tree, blockUntilReady } from '@jax-js/jax'; import {sampleCharacter} from './sampling.js'; export function rng(seed = 42) { let a = seed >>> 0; return () => { a += 0x6D2B79F5; 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; }; } export function tokenize(text) { const chars = [...new Set([...text])].sort(); if (chars.length < 2 || chars.length > 128) throw new Error('The dataset must contain 2 to 128 distinct characters.'); const index = new Map(chars.map((c, i) => [c, i])); const letters=[...text],paragraphStarts=[0]; for(let i=1;i index.get(c)), encode: s => [...s].map(c => { if (!index.has(c)) throw new Error(`Character missing from the dataset: ${JSON.stringify(c)}`); return index.get(c); }) }; } export function initParams(c, seed = 42) { const random = rng(seed); const weight = (shape, scale) => np.array(Float32Array.from({ length: shape.reduce((a,b) => a*b,1) }, () => (random()*2-1)*scale)).reshape(shape); if(c.architecture==='nanogpt') { // NanoGPT bias=False, dropout=0: LayerNorm, exact GELU, tied embedding/head. // Same distribution and residual scaling; JS/Python RNG streams differ. const normal=(shape,std=.02)=>np.array(Float32Array.from({length:shape.reduce((a,b)=>a*b,1)},()=>Math.sqrt(-2*Math.log(Math.max(1e-9,random())))*Math.cos(2*Math.PI*random())*std)).reshape(shape); const p={embed:normal([c.vocab,c.dim]),pos:normal([c.context,c.dim]),norm:np.ones([c.dim])}; for(let l=0;lnp.array(Float32Array.from({length:shape.reduce((a,b)=>a*b,1)},()=>Math.sqrt(-2*Math.log(Math.max(1e-9,random())))*Math.cos(2*Math.PI*random())*std)).reshape(shape); const zeroOrNormal=shape=>c.nonzeroInit?normal(shape,0.1):np.zeros(shape); const p={embed:normal([c.vocab,c.dim],1),norm:np.ones([c.dim]),head:zeroOrNormal([c.dim,c.vocab])}; for(let l=0;l x.ref.div(np.sqrt(np.mean(np.square(x),-1,{keepdims:true}).add(1e-5))).mul(w); const layerNorm=(x,w)=>{const centered=x.ref.sub(np.mean(x,-1,{keepdims:true}));return norm(centered,w);}; const rms = x => x.ref.div(np.sqrt(np.mean(np.square(x),-1,{keepdims:true}).add(1e-5))); function rotary(x,t,hd,full=false) { const angles=Array.from({length:t*hd/2},(_,i)=>{ const j=i%(hd/2),pos=Math.floor(i/(hd/2)); return full?pos*(1/10000)**(2*j/hd):jnp.zeros(p.shape),this.p); this.v=tree.map(p=>np.zeros(p.shape),this.p); this.lossGrad=jit(valueAndGrad((p,x,y)=>loss(p,x,y,c))); this.infer=jit((p,x)=>forward(p,x,c)); this.inferLast=jit((p,x,i)=>np.take(forward(p,x,c),i,1)); this.evaluate=jit((p,x,y)=>nn.logSoftmax(forward(p,x,c),-1).mul(nn.oneHot(y,c.vocab)).sum(-1).slice([], [Math.max(0,c.context-16),c.context]).mean().neg()); this.beta2=c.beta2??.999; this.update=jit((p,m,v,g,lr,b1,b2)=>{ // Global norm clipping before Adam. const total=Object.values(g).map(x=>np.square(x.ref).sum()).reduce((a,b)=>a.add(b)); const scale=np.minimum(1,np.reciprocal(np.sqrt(total).add(1e-8))); const nextP={},nextM={},nextV={}; for(const k of Object.keys(p)) { const grad=g[k].mul(scale.ref); const mm=m[k].mul(0.9).add(grad.ref.mul(0.1)); const vv=v[k].mul(this.beta2).add(np.square(grad).mul(1-this.beta2)); const decayed=p[k].mul(np.array(1).sub(lr.ref.mul(p[k].shape.length===2?(c.weightDecay??0):0))); nextP[k]=decayed.sub(mm.ref.div(b1.ref).div(np.sqrt(vv.ref.div(b2.ref)).add(1e-8)).mul(lr.ref)); nextM[k]=mm; nextV[k]=vv; } scale.dispose(); lr.dispose(); b1.dispose(); b2.dispose(); return [nextP,nextM,nextV]; }); } async train(x,y,rate=0.003) { const [l,g]=this.lossGrad(tree.ref(this.p),x,y); this.step++; [this.p,this.m,this.v]=this.update(this.p,this.m,this.v,g,np.array(rate),np.array(1-0.9**this.step),np.array(1-this.beta2**this.step)); await blockUntilReady([this.p,this.m,this.v]); const value=(await l.data())[0]; if(!Number.isFinite(value)) throw new Error('Non-finite loss. Reset and lower the learning rate.'); return value; } async generate(prefix,tokenizer,{length=80,temperature=0.8,minP=0,seed=123,bucketed=this.c.fastSampling??false,cancel=()=>false,onToken=()=>{}}={}) { const ids=tokenizer.encode(prefix); if(!ids.length) throw new Error('The prompt must not be empty.'); const random=rng(seed); let text=''; for(let i=0;isetTimeout(r,0)); } return text; } dispose(){tree.dispose([this.p,this.m,this.v]); this.lossGrad.dispose(); this.infer.dispose(); this.inferLast.dispose(); this.evaluate.dispose(); this.update.dispose();} }