Spaces:
Running
Running
File size: 10,134 Bytes
99eecc5 249a0e9 99eecc5 249a0e9 99eecc5 249a0e9 99eecc5 249a0e9 99eecc5 249a0e9 99eecc5 | 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 | 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<letters.length-1;i++)if(letters[i-1]==='\n'&&letters[i]==='\n'&&letters[i+1]!=='\n')paragraphStarts.push(i+1);
return { chars, paragraphStarts, ids: [...text].map(c => 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;l<c.layers;l++) {
for(const k of ['q','k','v'])p[`${l}.${k}`]=normal([c.dim,c.dim]);
p[`${l}.o`]=normal([c.dim,c.dim],.02/Math.sqrt(2*c.layers));
p[`${l}.up`]=normal([c.dim,4*c.dim]);
p[`${l}.down`]=normal([4*c.dim,c.dim],.02/Math.sqrt(2*c.layers));
p[`${l}.n1`]=np.ones([c.dim]);p[`${l}.n2`]=np.ones([c.dim]);
}
return p;
}
if(c.architecture==='modern') {
// Modded-inspired FP32 variant: Gaussian fan-in init, zero residual projections,
// RMSNorm, QK norm, RoPE, squared ReLU, soft-capped untied head. AdamW below.
const normal=(shape,std)=>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 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<c.layers;l++) {
for(const k of ['q','k','v'])p[`${l}.${k}`]=normal([c.dim,c.dim],Math.sqrt(.33/c.dim));
p[`${l}.o`]=zeroOrNormal([c.dim,c.dim]);
p[`${l}.up`]=normal([c.dim,4*c.dim],Math.sqrt(.33/c.dim));
p[`${l}.down`]=zeroOrNormal([4*c.dim,c.dim]);
p[`${l}.n1`]=np.ones([c.dim]);p[`${l}.n2`]=np.ones([c.dim]);
}
return p;
}
const p = { embed: weight([c.vocab,c.dim],0.2), pos: weight([c.context,c.dim],0.02) };
for(let l=0;l<c.layers;l++) {
for(const k of ['q','k','v','o']) p[`${l}.${k}`] = weight([c.dim,c.dim],Math.sqrt(3/c.dim));
p[`${l}.up`] = weight([c.dim,4*c.dim],Math.sqrt(3/c.dim));
p[`${l}.down`] = weight([4*c.dim,c.dim],Math.sqrt(3/(4*c.dim)));
p[`${l}.n1`] = np.ones([c.dim]); p[`${l}.n2`] = np.ones([c.dim]);
}
p.norm = np.ones([c.dim]); p.head = weight([c.dim,c.vocab],0.02);
// Keep the same RNG draws for other weights, for the positional ablation.
if(c.architecture==='rotary'){p.pos.dispose();delete p.pos;}
return p;
}
const norm = (x,w) => 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):j<hd/4?pos*(1/1024)**(j/Math.max(1,hd/4-1)):0;
});
const cs=np.array(angles.map(Math.cos)).reshape([1,t,1,hd/2]);
const sn=np.array(angles.map(Math.sin)).reshape([1,t,1,hd/2]);
const [a,b]=np.split(x,2,-1);
return np.concatenate([a.ref.mul(cs.ref).add(b.ref.mul(sn.ref)),b.mul(cs).sub(a.mul(sn))],-1);
}
// Each argument is consumed exactly once, including every parameter leaf.
export function forward(p, ids, c) {
const [b,t] = ids.shape, d = c.dim, h = c.heads;
const modern=c.architecture==='modern',rope=c.architecture==='rotary',nano=c.architecture==='nanogpt',normalize=nano?layerNorm:norm;
let x = np.matmul(nn.oneHot(ids,c.vocab),nano?p.embed.ref:p.embed);
if(!modern&&!rope)x=x.add(t===c.context?p.pos:p.pos.slice([0,t],[]));
for(let l=0;l<c.layers;l++) {
const n = normalize(x.ref,p[`${l}.n1`]);
let q = np.matmul(n.ref,p[`${l}.q`]).reshape([b,t,h,d/h]);
let k = np.matmul(n.ref,p[`${l}.k`]).reshape([b,t,h,d/h]);
const v = np.matmul(n,p[`${l}.v`]).reshape([b,t,h,d/h]);
if(modern){q=rotary(rms(q),t,d/h);k=rotary(rms(k),t,d/h);}
if(rope){q=rotary(q,t,d/h,true);k=rotary(k,t,d/h,true);}
const a = nn.dotProductAttention(q,k,v,{isCausal:true,...(modern?{scale:.12}:{})}).reshape([b,t,d]);
x = x.add(np.matmul(a,p[`${l}.o`]));
const z = normalize(x.ref,p[`${l}.n2`]);
const u=np.matmul(z,p[`${l}.up`]);
x = x.add(np.matmul(modern?np.square(nn.relu(u)):nn.gelu(u,{approximate:!nano}),p[`${l}.down`]));
}
const logits=np.matmul(normalize(x,p.norm),nano?p.embed.transpose():p.head);
return modern?logits.ref.mul(15).div(np.sqrt(np.square(logits).add(225))):logits;
}
export function loss(p,x,y,c) {
return nn.logSoftmax(forward(p,x,c),-1).mul(nn.oneHot(y,c.vocab)).sum(-1).mean().neg();
}
export function batch(ids,c,random) {
const x = [], y = [];
for(let b=0;b<c.batch;b++) {
const start = Math.floor(random()*(ids.length-c.context));
x.push(...ids.slice(start,start+c.context)); y.push(...ids.slice(start+1,start+c.context+1));
}
return [np.array(x,{dtype:np.int32}).reshape([c.batch,c.context]),np.array(y,{dtype:np.int32}).reshape([c.batch,c.context])];
}
// Paragraph starts use Unicode code-point offsets, like token ids.
// Circular continuation includes the last paragraph without padding fake targets.
export function paragraphBatch(ids,c,random,starts) {
if(!starts?.length)throw new Error('No paragraph starts available.');
const x=[],y=[];
for(let b=0;b<c.batch;b++){
const start=starts[Math.floor(random()*starts.length)];
for(let i=0;i<c.context;i++){x.push(ids[(start+i)%ids.length]);y.push(ids[(start+i+1)%ids.length]);}
}
return [np.array(x,{dtype:np.int32}).reshape([c.batch,c.context]),np.array(y,{dtype:np.int32}).reshape([c.batch,c.context])];
}
export class Trainer {
constructor(c,seed=42) {
this.c=c; this.p=initParams(c,seed); this.step=0;
this.m=tree.map(p=>np.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;i<length&&!cancel();i++) {
const ctx=ids.slice(-this.c.context), pos=ctx.length-1;
const size=bucketed?Math.min(this.c.context,Math.max(16,2**Math.ceil(Math.log2(ctx.length)))):this.c.context;
const padded=[...ctx,...Array(size-ctx.length).fill(0)];
const x=np.array(padded,{dtype:np.int32}).reshape([1,size]);
const logits=bucketed?this.inferLast(tree.ref(this.p),x,np.array(pos,{dtype:np.int32})):this.infer(tree.ref(this.p),x);
const raw=await logits.data(),values=bucketed?raw:raw.slice(pos*this.c.vocab,(pos+1)*this.c.vocab);
const choice=sampleCharacter(values,temperature,random,minP);
ids.push(choice); text+=tokenizer.chars[choice]; onToken(text);
if(i%8===0) await new Promise(r=>setTimeout(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();}
}
|