Initial commit

master
Freywar Ulvnaudgari 2 years ago
commit def3a479be
  1. 4
      .gitignore
  2. 59
      eslint.config.mjs
  3. 8
      jest.config.ts
  4. 40
      package.json
  5. 37
      shader/argmax.wgsl
  6. 33
      shader/attw.wgsl
  7. 32
      shader/avg.wgsl
  8. 18
      shader/base.wgsl
  9. 26
      shader/bscale.wgsl
  10. 21
      shader/cdiv.wgsl
  11. 21
      shader/cmul.wgsl
  12. 26
      shader/cscale.wgsl
  13. 21
      shader/csub.wgsl
  14. 21
      shader/csum.wgsl
  15. 34
      shader/dldo.wgsl
  16. 48
      shader/dlds.wgsl
  17. 43
      shader/dots.wgsl
  18. 30
      shader/embed.wgsl
  19. 47
      shader/gradinp.wgsl
  20. 28
      shader/gradtattw.wgsl
  21. 28
      shader/gradtout.wgsl
  22. 19
      shader/hotones.wgsl
  23. 37
      shader/isum.wgsl
  24. 31
      shader/mul.wgsl
  25. 29
      shader/otdldo.wgsl
  26. 39
      shader/samples.wgsl
  27. 25
      shader/softmaxexp.wgsl
  28. 25
      shader/softmaxnorm.wgsl
  29. 32
      shader/tot.wgsl
  30. 29
      shader/totdldowa.wgsl
  31. 20
      shader/transpose.wgsl
  32. 26
      shader/unavg.wgsl
  33. 301
      src/data/gpu/gpu-struct.ts
  34. 289
      src/data/gpu/matrix-batch.ts
  35. 233
      src/data/gpu/matrix.ts
  36. 176
      src/data/gpu/scalar-batch.ts
  37. 90
      src/data/gpu/scalar.ts
  38. 221
      src/data/gpu/vector-batch.ts
  39. 164
      src/data/gpu/vector.ts
  40. 100
      src/data/group-queue.ts
  41. 127
      src/data/list.ts
  42. 142
      src/data/math.ts
  43. 73
      src/data/trie.ts
  44. 196
      src/index.ts
  45. 261
      src/model-cpu.ts
  46. 662
      src/model-gpu.ts
  47. 160
      src/tokenizer.ts
  48. 307923
      sys
  49. 57
      test/group-queue.spec.ts
  50. 88
      test/list.spec.ts
  51. 7
      test/tsconfig.json
  52. 35
      tools/fetch.sh
  53. 178
      tools/stats.py
  54. 24
      tsconfig.json
  55. 3132
      yarn.lock

4
.gitignore vendored

@ -0,0 +1,4 @@
node_modules
build
data
models

@ -0,0 +1,59 @@
import { FlatCompat } from '@eslint/eslintrc'
import js from '@eslint/js'
import stylistic from '@stylistic/eslint-plugin'
import stylisticts from '@stylistic/eslint-plugin-ts'
import ts from '@typescript-eslint/eslint-plugin'
import parser from '@typescript-eslint/parser'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
export default [
...new FlatCompat({
baseDirectory: path.dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
})
.extends(
'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
),
stylistic.configs['recommended-flat'],
{
files: ['src/**/*.ts', 'test/**/*.ts'],
languageOptions: { parser },
plugins: {
'@typescript-eslint': ts,
'@stylistic': stylistic,
'@stylistic/ts': stylisticts,
},
rules: {
'arrow-body-style': 'error',
'no-extra-parens': ['error', 'all'],
'no-unexpected-multiline': 'off',
'sort-imports': ['error', { ignoreDeclarationSort: true }],
'@stylistic/brace-style': ['error', '1tbs', { allowSingleLine: true }],
'@stylistic/quotes': ['error', 'single'],
'@stylistic/semi': ['error', 'always'],
'@stylistic/ts/semi': ['error', 'always'],
'@stylistic/indent': 'error',
'@stylistic/arrow-parens': ['error', 'as-needed', { requireForBlockBody: false }],
'@stylistic/max-statements-per-line': 'off',
'@stylistic/member-delimiter-style': 'off',
'@stylistic/ts/member-delimiter-style': 'error',
'@stylistic/no-mixed-operators': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{
args: 'all',
argsIgnorePattern: '^_',
caughtErrors: 'all',
caughtErrorsIgnorePattern: '^_',
destructuredArrayIgnorePattern: '^_',
varsIgnorePattern: '^_',
ignoreRestSiblings: true,
},
],
'@typescript-eslint/no-explicit-any': 'off',
},
}]

@ -0,0 +1,8 @@
import type { Config } from 'jest'
export default {
testEnvironment: 'node',
transform: {
'^.+.tsx?$': ['ts-jest', { tsconfig: 'test/tsconfig.json' }],
},
} as Config

@ -0,0 +1,40 @@
{
"name": "nn",
"version": "1.0.0",
"type": "module",
"main": "index.js",
"author": "Freywar Ulvnaudgari <freywar.ulvnaudgari@gmail.com>",
"license": "MIT",
"scripts": {
"build": "rm -rf build && tsc",
"start": "yarn build && systemd-inhibit --what=idle:sleep --why='Training a model' node --import=extensionless/register build/index.js",
"start:debug": "yarn build && node --inspect-brk --import=extensionless/register build/index.js",
"start:profile": "yarn build && node --prof --import=extensionless/register build/index.js",
"test": "jest",
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand",
"lint": "eslint",
"lint:fix": "eslint --fix"
},
"devDependencies": {
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.8.0",
"@jest/globals": "^29.7.0",
"@stylistic/eslint-plugin": "^2.6.1",
"@stylistic/eslint-plugin-ts": "^2.6.4",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"eslint": "^9.8.0",
"extensionless": "^1.9.9",
"jest": "^29.7.0",
"ts-jest": "^29.2.3",
"ts-node": "^10.9.2",
"typescript": "^5.5.4"
},
"dependencies": {
"@webgpu/glslang": "^0.0.15",
"@webgpu/types": "^0.1.60",
"ts-command-line-args": "^2.5.1",
"webgpu": "^0.2.11",
"yargs": "^17.7.2"
}
}

@ -0,0 +1,37 @@
struct MatrixBatch {
size: vec4<u32>,
data: array<f32>,
}
struct FVectorBatch {
size: vec4<u32>,
data: array<f32>,
}
struct UVectorBatch {
size: vec4<u32>,
data: array<u32>,
}
@group(0) @binding(0)
var<storage, read> ps: MatrixBatch;
@group(0) @binding(1)
var<storage, read_write> result: UVectorBatch;
@compute @workgroup_size(1)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
result.size.x = ps.size.y;
result.size.z = ps.size.z;
var mi = 0u;
var mp = 0f;
var pi = global_id.z * ps.size.y * ps.size.x + global_id.x * ps.size.x;
for (var i = 0u; i < ps.size.x; i++) {
if (ps.data[pi] > mp) {
mi = i;
mp = ps.data[pi];
}
pi++;
}
result.data[global_id.z * result.size.x + global_id.x] = mi;
}

@ -0,0 +1,33 @@
struct PackedMatrixBatch {
size: vec4<u32>,
data: array<vec4<f32>>,
}
struct MatrixBatch {
size: vec4<u32>,
data: array<f32>,
}
@group(0) @binding(0)
var<storage, read> e: PackedMatrixBatch;
@group(0) @binding(1)
var<storage, read> tw: PackedMatrixBatch;
@group(0) @binding(2)
var<storage, read_write> aw: MatrixBatch;
@compute @workgroup_size(1)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
aw.size.x = tw.size.y;
aw.size.y = e.size.y;
aw.size.z = e.size.z;
var s = 0f;
let vc = e.size.x / 4;
let lo = (global_id.z * e.size.y + global_id.y) * vc;
let ro = (global_id.z * tw.size.y + global_id.x) * vc;
for (var i = 0u; i < vc; i++) {
s += dot(e.data[lo + i], tw.data[ro + i]);
}
aw.data[(global_id.z * aw.size.y + global_id.y) * aw.size.x + global_id.x] = s;
}

@ -0,0 +1,32 @@
alias number = f32;
struct MatrixBatch {
size: vec4<u32>,
data: array<vec4<number>>,
}
struct Matrix {
size: vec4<u32>,
data: array<vec4<number>>,
}
@group(0) @binding(0)
var<storage, read> m: MatrixBatch;
@group(0) @binding(1)
var<storage, read_write> result: Matrix;
@compute @workgroup_size(1)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
result.size = m.size;
result.size.z = 1;
var vc = result.size.x / 4;
var resi = global_id.y * vc + global_id.x;
result.data[resi] = vec4(0f);
var mi = resi;
for (var z = 0u; z < m.size.z; z++) {
result.data[resi] += m.data[mi] / vec4(f32(m.size.z));
mi += m.size.y * vc;
}
}

@ -0,0 +1,18 @@
struct MatrixBatch {
size: vec4<u32>,
data: array<f32>,
}
@group(0) @binding(3)
var<storage, read_write> m: MatrixBatch;
@compute @workgroup_size(1)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
for (var z = 0u; z < m.size.z; z++) {
for (var y = 0u; y < m.size.y; y++) {
for (var x = 0u; x < m.size.x; x++) {
m.data[z * m.size.y * m.size.x + y * m.size.x + x] = 0f;
}
}
}
}

@ -0,0 +1,26 @@
alias number = f32;
struct ScalarBatch {
size: vec4<u32>,
data: array<number>,
}
struct MatrixBatch {
size: vec4<u32>,
data: array<vec4<number>>,
}
@group(0) @binding(0)
var<storage, read> l: ScalarBatch;
@group(0) @binding(1)
var<storage, read> r_or_result: MatrixBatch;
@group(0) @binding(2)
var<storage, read_write> result: MatrixBatch;
@compute @workgroup_size(1)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
result.size = r_or_result.size;
var resi = (global_id.z * result.size.y + global_id.y) * result.size.x / 4 + global_id.x;
result.data[resi] = l.data[global_id.z] * r_or_result.data[resi];
}

@ -0,0 +1,21 @@
alias number = f32;
struct MatrixBatch {
size: vec4<u32>,
data: array<vec4<number>>,
}
@group(0) @binding(0)
var<storage, read> l: MatrixBatch;
@group(0) @binding(1)
var<storage, read> r_or_result: MatrixBatch;
@group(0) @binding(2)
var<storage, read_write> result: MatrixBatch;
@compute @workgroup_size(1)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
result.size = l.size;
var resi = (global_id.z * result.size.y + global_id.y) * result.size.x / 4 + global_id.x;
result.data[resi] = l.data[resi] / r_or_result.data[resi];
}

@ -0,0 +1,21 @@
alias number = f32;
struct MatrixBatch {
size: vec4<u32>,
data: array<vec4<number>>,
}
@group(0) @binding(0)
var<storage, read> l: MatrixBatch;
@group(0) @binding(1)
var<storage, read> r_or_result: MatrixBatch;
@group(0) @binding(2)
var<storage, read_write> result: MatrixBatch;
@compute @workgroup_size(1)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
result.size = l.size;
var resi = (global_id.z * result.size.y + global_id.y) * result.size.x / 4 + global_id.x;
result.data[resi] = l.data[resi] * r_or_result.data[resi];
}

@ -0,0 +1,26 @@
alias number = f32;
struct Scalar {
size: vec4<u32>,
data: array<number>,
}
struct MatrixBatch {
size: vec4<u32>,
data: array<vec4<number>>,
}
@group(0) @binding(0)
var<storage, read> l: Scalar;
@group(0) @binding(1)
var<storage, read> r_or_result: MatrixBatch;
@group(0) @binding(2)
var<storage, read_write> result: MatrixBatch;
@compute @workgroup_size(1)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
result.size = r_or_result.size;
var resi = (global_id.z * result.size.y + global_id.y) * result.size.x / 4 + global_id.x;
result.data[resi] = l.data[0] * r_or_result.data[resi];
}

@ -0,0 +1,21 @@
alias number = f32;
struct MatrixBatch {
size: vec4<u32>,
data: array<vec4<number>>,
}
@group(0) @binding(0)
var<storage, read> l: MatrixBatch;
@group(0) @binding(1)
var<storage, read> r_or_result: MatrixBatch;
@group(0) @binding(2)
var<storage, read_write> result: MatrixBatch;
@compute @workgroup_size(1)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
result.size = l.size;
var resi = (global_id.z * result.size.y + global_id.y) * result.size.x / 4 + global_id.x;
result.data[resi] = l.data[resi] - r_or_result.data[resi];
}

@ -0,0 +1,21 @@
alias number = f32;
struct MatrixBatch {
size: vec4<u32>,
data: array<vec4<number>>,
}
@group(0) @binding(0)
var<storage, read> l: MatrixBatch;
@group(0) @binding(1)
var<storage, read> r_or_result: MatrixBatch;
@group(0) @binding(2)
var<storage, read_write> result: MatrixBatch;
@compute @workgroup_size(1)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
result.size = l.size;
var resi = (global_id.z * result.size.y + global_id.y) * result.size.x / 4 + global_id.x;
result.data[resi] = l.data[resi] + r_or_result.data[resi];
}

@ -0,0 +1,34 @@
alias number = f32;
struct Scalar {
size: vec4<u32>,
data: array<number>,
}
struct MatrixBatch {
size: vec4<u32>,
data: array<number>,
}
struct UVectorBatch {
size: vec4<u32>,
data: array<u32>,
}
@group(0) @binding(0)
var<storage, read> m: MatrixBatch;
@group(0) @binding(1)
var<storage, read> v: UVectorBatch;
@group(0) @binding(2)
var<storage, read> lr: Scalar;
@group(0) @binding(3)
var<storage, read_write> grs: MatrixBatch;
@compute @workgroup_size(1)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
grs.size = m.size;
var vi = global_id.z * v.size.x + global_id.y;
var gi = vi * grs.size.x + global_id.x;
grs.data[gi] = lr.data[0] * (m.data[gi] - select(0f, 1f, v.data[vi] == global_id.x));
}

@ -0,0 +1,48 @@
struct Scalar {
size: vec4<u32>,
data: array<f32>,
}
struct MatrixBatch {
size: vec4<u32>,
data: array<f32>,
}
@group(0) @binding(0)
var<storage, read> ds: Scalar;
@group(0) @binding(1)
var<storage, read> wa: MatrixBatch;
@group(0) @binding(2)
var<storage, read> v: MatrixBatch;
@group(0) @binding(3)
var<storage, read> otdldo: MatrixBatch;
@group(0) @binding(4)
var<storage, read_write> dlds: MatrixBatch;
@compute @workgroup_size(1)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
dlds.size = wa.size;
var s = 0f;
var li = (global_id.z * v.size.y + global_id.x) * v.size.x;
var ri = global_id.z * otdldo.size.x * otdldo.size.y + global_id.y;
for (var i = 0u; i < v.size.x; i++) {
var cs = 0f;
var lj = global_id.z * wa.size.y * wa.size.x + global_id.y * wa.size.x;
var mj = global_id.z * v.size.x * v.size.y + i;
for (var j = 0u; j < wa.size.x; j++) {
cs += wa.data[lj] * v.data[mj];
lj++;
mj += v.size.x;
}
s += (v.data[li] - cs) * otdldo.data[ri];
li++;
ri += otdldo.size.x;
}
let resi = (global_id.z * dlds.size.y + global_id.y) * dlds.size.x + global_id.x;
dlds.data[resi] = ds.data[0] * wa.data[resi] * s;
}

@ -0,0 +1,43 @@
struct Scalar {
size: vec4<u32>,
data: array<f32>,
}
struct Matrix {
size: vec4<u32>,
data: array<vec4<f32>>,
}
struct MatrixBatch {
size: vec4<u32>,
data: array<vec4<f32>>,
}
@group(0) @binding(0)
var<storage, read> ebs: MatrixBatch;
@group(0) @binding(1)
var<storage, read> ows: Matrix;
@group(0) @binding(2)
var<storage, read> tmp: Scalar;
@group(0) @binding(3)
var<storage, read_write> dts: MatrixBatch;
@compute @workgroup_size(1)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
dts.size.x = ows.size.y;
dts.size.y = ebs.size.y;
dts.size.z = ebs.size.z;
let vc = ebs.size.x / 4;
let eo = (global_id.z * ebs.size.y + global_id.y) * vc;
let oo = 4 * global_id.x * vc;
var s = vec4(0f);
for (var i = 0u; i < vc; i++) {
let eb = ebs.data[eo + i];
s.x += dot(eb, ows.data[oo + 0 * vc + i]);
s.y += dot(eb, ows.data[oo + 1 * vc + i]);
s.z += dot(eb, ows.data[oo + 2 * vc + i]);
s.w += dot(eb, ows.data[oo + 3 * vc + i]);
}
dts.data[(global_id.z * dts.size.y + global_id.y) * dts.size.x / 4 + global_id.x] = tmp.data[0] * s;
}

@ -0,0 +1,30 @@
struct MatrixBatch {
size: vec4<u32>,
data: array<f32>,
}
@group(0) @binding(0)
var<storage, read> ilogits: MatrixBatch;
@group(0) @binding(1)
var<storage, read> inp: MatrixBatch;
@group(0) @binding(2)
var<storage, read> pos: MatrixBatch;
@group(0) @binding(3)
var<storage, read_write> r: MatrixBatch;
@compute @workgroup_size(1)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
r.size = pos.size;
var s = 0f;
var li = (global_id.z * ilogits.size.y + global_id.y) * ilogits.size.x;
var ri = global_id.z * inp.size.x * inp.size.y + global_id.x;
for (var i = 0u; i < ilogits.size.x; i++) {
s += ilogits.data[li] * inp.data[ri];
li++;
ri += inp.size.x;
}
var resi = (global_id.z * r.size.y + global_id.y) * r.size.x + global_id.x;
r.data[resi] = s + pos.data[resi];
}

@ -0,0 +1,47 @@
struct MatrixBatch {
size: vec4<u32>,
data: array<f32>,
}
@group(0) @binding(0)
var<storage, read> ilogits: MatrixBatch;
@group(0) @binding(0)
var<storage, read> tilogits: MatrixBatch;
@group(0) @binding(1)
var<storage, read> dldsq: MatrixBatch;
@group(0) @binding(2)
var<storage, read> dldsk: MatrixBatch;
@group(0) @binding(3)
var<storage, read> totdldowa: MatrixBatch;
@group(0) @binding(4)
var<storage, read> tqry: MatrixBatch;
@group(0) @binding(5)
var<storage, read> tkey: MatrixBatch;
@group(0) @binding(6)
var<storage, read> tval: MatrixBatch;
@group(0) @binding(7)
var<storage, read_write> ginp: MatrixBatch;
@compute @workgroup_size(1)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
ginp.size.x = tval.size.x;
ginp.size.y = ilogits.size.x;
ginp.size.z = ilogits.size.z;
var ts = 0f;
var li = global_id.z * ilogits.size.y * ilogits.size.x + global_id.y;
for (var i = 0u; i < ilogits.size.y; i++) {
var s = 0f;
var lj = global_id.z * totdldowa.size.x * totdldowa.size.y + i * totdldowa.size.x;
var rj = global_id.z * tval.size.x * tval.size.y + global_id.x;
for (var j = 0u; j < totdldowa.size.x; j++) {
s += totdldowa.data[lj] * tval.data[rj] + dldsq.data[lj] * tkey.data[rj] + dldsk.data[lj] * tqry.data[rj];
lj++;
rj += tval.size.x;
}
ts += ilogits.data[li] * s;
li += ilogits.size.x;
}
ginp.data[global_id.z * ginp.size.y * ginp.size.x + global_id.y * ginp.size.x + global_id.x] = ts;
}

@ -0,0 +1,28 @@
struct MatrixBatch {
size: vec4<u32>,
data: array<f32>,
}
@group(0) @binding(0)
var<storage, read> e: MatrixBatch;
@group(0) @binding(1)
var<storage, read> dlds: MatrixBatch;
@group(0) @binding(2)
var<storage, read_write> gtattw: MatrixBatch;
@compute @workgroup_size(1)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
gtattw.size.z = e.size.z;
gtattw.size.y = e.size.x;
gtattw.size.x = e.size.x;
var s = 0f;
var li = global_id.z * e.size.x * e.size.y + global_id.x;
var ri = global_id.z * dlds.size.x * dlds.size.y + global_id.y;
for (var i = 0u; i < e.size.y; i++) {
s += e.data[li] * dlds.data[ri];
li += e.size.x;
ri += dlds.size.x;
}
gtattw.data[global_id.z * gtattw.size.y * gtattw.size.x + global_id.y * gtattw.size.x + global_id.x] = s;
}

@ -0,0 +1,28 @@
struct MatrixBatch {
size: vec4<u32>,
data: array<f32>,
}
@group(0) @binding(0)
var<storage, read> a: MatrixBatch;
@group(0) @binding(1)
var<storage, read> dldo: MatrixBatch;
@group(0) @binding(2)
var<storage, read_write> gtout: MatrixBatch;
@compute @workgroup_size(1)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
gtout.size = a.size;
gtout.size.y = dldo.size.x;
var s = 0f;
var ai = global_id.z * a.size.y * a.size.x + global_id.x;
var di = global_id.z * dldo.size.x * dldo.size.y + global_id.y;
for (var i = 0u; i < a.size.y; i++) {
s += a.data[ai] * dldo.data[di];
ai += a.size.x;
di += dldo.size.x;
}
gtout.data[global_id.z * gtout.size.y * gtout.size.x + global_id.y * gtout.size.x + global_id.x] = s;
}

@ -0,0 +1,19 @@
struct UVectorBatch {
size: vec4<u32>,
data: array<u32>,
}
struct MatrixBatch {
size: vec4<u32>,
data: array<f32>,
}
@group(0) @binding(0)
var<storage, read> v: UVectorBatch;
@group(0) @binding(1)
var<storage, read_write> result: MatrixBatch;
@compute @workgroup_size(1)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
result.data[global_id.z * result.size.y * result.size.x + global_id.y * result.size.x + global_id.x] = select(0f, 1f, global_id.x == v.data[global_id.z * v.size.x + global_id.y]);
}

@ -0,0 +1,37 @@
alias number = f32;
struct VectorBatch {
size: vec4<u32>,
data: array<u32>,
}
struct MatrixBatch {
size: vec4<u32>,
data: array<vec4<number>>,
}
@group(0) @binding(0)
var<storage, read> v: VectorBatch;
@group(0) @binding(1)
var<storage, read> m: MatrixBatch;
@group(0) @binding(2)
var<storage, read_write> result: MatrixBatch;
@compute @workgroup_size(1)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
result.size = m.size;
var vc = result.size.x / 4;
var resi = (global_id.z * result.size.y + global_id.y) * vc + global_id.x;
result.data[resi] = vec4(0f);
var mi = global_id.z * m.size.y * vc + global_id.x;
var vi = global_id.z * v.size.x;
for (var i = 0u; i < v.size.x; i++) {
if (global_id.y == v.data[vi]) {
result.data[resi] += m.data[mi];
}
mi += vc;
vi++;
}
}

@ -0,0 +1,31 @@
alias number = f32;
struct MatrixBatch {
size: vec4<u32>,
data: array<number>,
}
@group(0) @binding(0)
var<storage, read> l: MatrixBatch;
@group(0) @binding(1)
var<storage, read> r: MatrixBatch;
@group(0) @binding(2)
var<storage, read_write> result: MatrixBatch;
@compute @workgroup_size(1)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
result.size.x = r.size.x;
result.size.y = l.size.y;
result.size.z = l.size.z;
var s = 0f;
var li = global_id.z * l.size.x * l.size.y + global_id.y * l.size.x;
var ri = global_id.z * r.size.x * r.size.y + global_id.x;
for (var i = 0u; i < l.size.x; i++) {
s += l.data[li] * r.data[ri];
li++;
ri += r.size.x;
}
result.data[global_id.z * result.size.y * result.size.x + global_id.y * result.size.x + global_id.x] = s;
}

@ -0,0 +1,29 @@
struct MatrixBatch {
size: vec4<u32>,
data: array<f32>,
}
@group(0) @binding(0)
var<storage, read> dldo: MatrixBatch;
@group(0) @binding(1)
var<storage, read> tout: MatrixBatch;
@group(0) @binding(2)
var<storage, read_write> otdldo: MatrixBatch;
@compute @workgroup_size(1)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
otdldo.size.x = dldo.size.y;
otdldo.size.y = tout.size.x;
otdldo.size.z = dldo.size.z;
var s = 0f;
var li = (global_id.z * dldo.size.y + global_id.x) * dldo.size.x;
var ri = global_id.z * tout.size.x * tout.size.y + global_id.y;
for (var i = 0u; i < dldo.size.x; i++) {
s += dldo.data[li] * tout.data[ri];
li++;
ri += tout.size.x;
}
otdldo.data[(global_id.z * otdldo.size.y + global_id.y) * otdldo.size.x + global_id.x] = s;
}

@ -0,0 +1,39 @@
struct MatrixBatch {
size: vec4<u32>,
data: array<f32>,
}
struct FVectorBatch {
size: vec4<u32>,
data: array<f32>,
}
struct UVectorBatch {
size: vec4<u32>,
data: array<u32>,
}
@group(0) @binding(0)
var<storage, read> m: MatrixBatch;
@group(0) @binding(1)
var<storage, read> r: FVectorBatch;
@group(0) @binding(2)
var<storage, read_write> result: UVectorBatch;
@compute @workgroup_size(1)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
result.size.x = m.size.y;
result.size.z = m.size.z;
var resi = global_id.z * result.size.x + global_id.x;
var tp = r.data[global_id.z * r.size.x + global_id.x];
var p = 0f;
var mi = (global_id.z * m.size.y + global_id.x) * m.size.x;
for (var i = 0u; i < m.size.x; i++) {
p += m.data[mi];
if (p >= tp) {
result.data[resi] = i;
break;
}
mi++;
}
}

@ -0,0 +1,25 @@
struct MatrixBatch {
size: vec4<u32>,
data: array<vec4<f32>>,
}
@group(0) @binding(0)
var<storage, read> b: MatrixBatch;
@group(0) @binding(1)
var<storage, read_write> result: MatrixBatch;
@compute @workgroup_size(1)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
result.size = b.size;
var vc = result.size.x / 4;
var resi = (global_id.z * result.size.y + global_id.y) * vc + global_id.x;
var mv = b.data[resi];
var bi = (global_id.z * b.size.y + global_id.y) * vc;
for (var i = 0u; i < vc; i++) {
mv = max(mv, b.data[bi]);
bi++;
}
var ms = max(max(mv.x, mv.y), max(mv.z, mv.w));
result.data[resi] = exp(b.data[resi] - vec4(ms));
}

@ -0,0 +1,25 @@
struct MatrixBatch {
size: vec4<u32>,
data: array<vec4<f32>>,
}
@group(0) @binding(0)
var<storage, read> b: MatrixBatch;
@group(0) @binding(1)
var<storage, read_write> result: MatrixBatch;
@compute @workgroup_size(1)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
result.size = b.size;
var vc = result.size.x / 4;
var resi = (global_id.z * result.size.y + global_id.y) * vc + global_id.x;
var sv = vec4(0f);
var bi = (global_id.z * b.size.y + global_id.y) * vc;
for (var i = 0u; i < vc; i++) {
sv += b.data[bi];
bi++;
}
var ss = sv.x + sv.y + sv.z + sv.w;
result.data[resi] = b.data[resi] / vec4(ss);
}

@ -0,0 +1,32 @@
alias number = f32;
struct MatrixBatch {
size: vec4<u32>,
data: array<vec4<number>>,
}
struct Matrix {
size: vec4<u32>,
data: array<vec4<number>>,
}
@group(0) @binding(0)
var<storage, read> m: MatrixBatch;
@group(0) @binding(1)
var<storage, read_write> result: Matrix;
@compute @workgroup_size(1)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
result.size = m.size;
result.size.z = 1;
var vc = result.size.x / 4;
var resi = global_id.y * vc + global_id.x;
result.data[resi] = vec4(0f);
var mi = resi;
for (var z = 0u; z < m.size.z; z++) {
result.data[resi] += m.data[mi];
mi += m.size.y * vc;
}
}

@ -0,0 +1,29 @@
struct MatrixBatch {
size: vec4<u32>,
data: array<f32>,
}
@group(0) @binding(0)
var<storage, read> wa: MatrixBatch;
@group(0) @binding(1)
var<storage, read> otdldo: MatrixBatch;
@group(0) @binding(2)
var<storage, read_write> totdldowa: MatrixBatch;
@compute @workgroup_size(1)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
totdldowa.size.x = otdldo.size.y;
totdldowa.size.y = wa.size.x;
totdldowa.size.z = otdldo.size.z;
var s = 0f;
var li = global_id.z * otdldo.size.x * otdldo.size.y + global_id.x * otdldo.size.x;
var ri = global_id.z * wa.size.x * wa.size.y + global_id.y;
for (var i = 0u; i < otdldo.size.x; i++) {
s += otdldo.data[li] * wa.data[ri];
li++;
ri += wa.size.x;
}
totdldowa.data[(global_id.z * totdldowa.size.y + global_id.y) * totdldowa.size.x + global_id.x] = s;
}

@ -0,0 +1,20 @@
alias number = f32;
struct MatrixBatch {
size: vec4<u32>,
data: array<number>,
}
@group(0) @binding(0)
var<storage, read> in: MatrixBatch;
@group(0) @binding(1)
var<storage, read_write> out: MatrixBatch;
@compute @workgroup_size(1)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
out.size.x = in.size.y;
out.size.y = in.size.x;
out.size.z = in.size.z;
var l = global_id.z * out.size.y * out.size.x;
out.data[l + global_id.y * out.size.x + global_id.x] = in.data[l + global_id.x * in.size.x + global_id.y];
}

@ -0,0 +1,26 @@
alias number = f32;
struct Matrix {
size: vec4<u32>,
data: array<vec4<number>>,
}
struct MatrixBatch {
size: vec4<u32>,
data: array<vec4<number>>,
}
@group(0) @binding(0)
var<storage, read> m: Matrix;
@group(0) @binding(1)
var<storage, read_write> result: MatrixBatch;
@compute @workgroup_size(1)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>, @builtin(num_workgroups) num_workgroups: vec3<u32>) {
result.size = m.size;
result.size.z = num_workgroups.z * 2;
var vc = result.size.x / 4;
var mi = global_id.y * vc + global_id.x;
result.data[global_id.z * result.size.y * vc + mi] = m.data[mi];
}

@ -0,0 +1,301 @@
import fs from 'node:fs';
import type { Scalar } from './scalar';
interface ComputePipeline {
pipeline: GPUComputePipeline;
layout: GPUBindGroupLayout;
}
const PIPELINES: Map<GPUDevice, Map<string, ComputePipeline>> = new Map();
const COMPWISE_SHADER = {
sum: fs.readFileSync('shader/csum.wgsl').toString(),
sub: fs.readFileSync('shader/csub.wgsl').toString(),
mul: fs.readFileSync('shader/cmul.wgsl').toString(),
div: fs.readFileSync('shader/cdiv.wgsl').toString(),
scale: fs.readFileSync('shader/cscale.wgsl').toString(),
};
export function createComputePipeline(
device: GPUDevice,
name: string,
shader: () => string,
entry: string = 'main',
) {
if (!PIPELINES.has(device)) {
PIPELINES.set(device, new Map());
}
const d = PIPELINES.get(device)!;
if (!d.has(name)) {
const pipeline = device.createComputePipeline({
label: name,
layout: 'auto',
compute: {
module: device.createShaderModule({ code: shader() }),
entryPoint: entry,
},
});
const layout = pipeline.getBindGroupLayout(0);
d.set(name, { pipeline, layout });
}
return d.get(name)!;
}
export abstract class GPUStruct<A extends Uint32Array | Int32Array | Float32Array> {
public static dunit = 8;
public static lunit = 2;
protected readonly _label: string;
protected readonly _device: GPUDevice;
protected readonly _array: new (size: number | ArrayBufferLike | ArrayLike<number>) => A;
protected readonly _inbuffer: GPUBuffer;
protected readonly _dummybuffer: GPUBuffer;
protected readonly _outbuffer: GPUBuffer;
protected _op: 'reading' | 'writing' | null = null;
protected _dirty: 'front' | 'back' | null = 'front';
protected abstract workgroups(pm?: number): [number, number, number];
protected abstract workgroup(): [number, number, number];
public readonly size: Int32Array;
public readonly data: A;
public get type(): 'u32' | 'i32' | 'f32' {
switch (this._array as unknown) {
case Uint32Array: return 'u32';
case Int32Array: return 'i32';
case Float32Array: return 'f32';
default: throw new Error('Unknown array type.');
}
}
public get length(): number {
return this.data.length;
}
public get byteLength(): number {
return this.size.byteLength + this.data.byteLength;
}
public constructor(
device: GPUDevice,
array: new (size: number) => A,
label: string,
size: [number, number, number, number],
) {
this._device = device;
this._array = array;
this._label = label ?? 'struct';
this.size = new Int32Array(size);
this.data = new array(size[0] * size[1] * size[2] * size[3]);
this._inbuffer = device.createBuffer({
label: `${this._label}in`,
size: Math.max(32, this.byteLength),
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
});
this._dummybuffer = device.createBuffer({
label: `${label}dummy`,
size: 0,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
});
this._outbuffer = this._device.createBuffer({
label: `${label}out`,
size: Math.max(32, this.byteLength),
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
});
}
public write(): void {
if (!this._dirty) {
return;
}
if (this._op) {
throw new Error('Sync in progress.');
}
this._op = 'writing';
this._device.queue.writeBuffer(this._inbuffer, 0, new Uint8Array(this.size.buffer));
this._device.queue.writeBuffer(this._inbuffer, this.size.byteLength, new Uint8Array(this.data.buffer));
this._dirty = null;
this._op = null;
}
public async read(): Promise<void> {
if (!this._dirty) {
return;
}
if (this._op) {
throw new Error('Sync in progress.');
}
this._op = 'reading';
const copy = this._device.createCommandEncoder();
copy.copyBufferToBuffer(this._inbuffer, 0, this._outbuffer, 0, this.byteLength);
this._device.queue.submit([copy.finish()]);
await this._device.queue.onSubmittedWorkDone();
await this._outbuffer.mapAsync(GPUMapMode.READ);
this.data.set(new this._array(this._outbuffer.getMappedRange()).slice(this.size.length));
this._outbuffer.unmap();
this._dirty = null;
this._op = null;
}
public buffer(): GPUBuffer {
if (this._dirty === 'front') {
this.write();
}
return this._inbuffer;
}
protected workgroupify(shader: string): string {
const [x, y, z] = this.workgroup();
if (!x || !y || !z) {
throw new Error(`Invalid workgroup size: (${x}, ${y}, ${z}).`);
}
return shader.replace(/@workgroup_size\(.*?\)/g, `@workgroup_size(${x}, ${y}, ${z})`);
}
protected inplacify(shader: string): string {
let r = '';
return shader.replace(/\w+_or_result/g, v => r = r ? 'result' : v); // The first occurrence must be the declaration.
}
protected typify(shader: string): string {
return shader.replace(/alias number = .+?;/g, `alias number = ${this.type};`);
}
public calculate(
name: string,
shader: string,
args: GPUStruct<Uint32Array | Int32Array | Float32Array>[],
ip = false,
pm = 1,
): typeof this {
const encoder = this._device.createCommandEncoder();
const pass = encoder.beginComputePass();
const { pipeline, layout } = createComputePipeline(this._device, `${name}${ip ? '_inplace_' : '_'}${this.type}`, () => this.workgroupify(this.typify(ip ? this.inplacify(shader) : shader)));
pass.setPipeline(pipeline);
pass.setBindGroup(0, this._device.createBindGroup({
layout, entries: [
...args.map((s, i) => ({ binding: i, resource: { buffer: s.buffer() } })),
{ binding: args.length + +ip, resource: { buffer: this.buffer() } },
],
}));
const [x, y, z] = this.workgroups(pm);
if (!x || !y || !z || Math.floor(x) !== x || Math.floor(y) !== y || Math.floor(z) !== z) {
throw new Error(`Invalid workgroup count: (${x}, ${y}, ${z}).`);
}
pass.dispatchWorkgroups(x, y, z);
pass.end();
this._device.queue.submit([encoder.finish()]);
this._dirty = 'back';
return this;
}
protected static assertUnique(...ms: GPUStruct<Uint32Array | Int32Array | Float32Array>[]): void {
const map: Map<GPUStruct<Uint32Array | Int32Array | Float32Array>, boolean> = new Map();
for (const m of ms) {
if (map.has(m)) {
throw new Error('Can not use the same GPU structure multiple times.');
}
map.set(m, true);
}
}
protected static assertColocated(...ms: GPUStruct<Uint32Array | Int32Array | Float32Array>[]): void {
const map: Map<GPUDevice, boolean> = new Map();
for (const m of ms) {
map.set(m._device, true);
if (map.size > 1) {
throw new Error('Can not use GPU structures stored on multiple devices.');
}
}
}
protected static assertSimilar(...ms: GPUStruct<Uint32Array | Int32Array | Float32Array>[]): void {
for (const m of ms) {
if (m.length !== ms[0].length
|| m.workgroup()[0] !== ms[0].workgroup()[0]
|| m.workgroup()[1] !== ms[0].workgroup()[1]
|| m.workgroup()[2] !== ms[0].workgroup()[2]) {
throw new Error('Can not use GPU structures with different dimensions.');
}
}
}
protected static assertPackable(pm: number, ...ms: GPUStruct<Uint32Array | Int32Array | Float32Array>[]): void {
for (const m of ms) {
if (m.workgroups(pm).some(v => v < 1)) {
throw new Error(`Struct ${m._label} size must be divisible by ${pm}.`);
}
}
}
public static compwise<
A extends Uint32Array | Int32Array | Float32Array,
S extends GPUStruct<A>,
>(op: keyof typeof COMPWISE_SHADER, l: S, r: S, result: S): S {
this.assertUnique(l, r);
this.assertUnique(l, result);
this.assertColocated(l, r, result);
this.assertPackable(4, l, r, result);
return result.calculate(op, COMPWISE_SHADER[op], result === r ? [l] : [l, r], result === r, 4);
}
public static csum<
A extends Uint32Array | Int32Array | Float32Array,
S extends GPUStruct<A>,
>(l: S, r: S, result: S): S {
return GPUStruct.compwise('sum', l, r, result);
}
public static csub<
A extends Uint32Array | Int32Array | Float32Array,
S extends GPUStruct<A>,
>(l: S, r: S, result: S): S {
return GPUStruct.compwise('sub', l, r, result);
}
public static cmul<
A extends Uint32Array | Int32Array | Float32Array,
S extends GPUStruct<A>,
>(l: S, r: S, result: S): S {
return GPUStruct.compwise('mul', l, r, result);
}
public static cdiv<
A extends Uint32Array | Int32Array | Float32Array,
S extends GPUStruct<A>,
>(l: S, r: S, result: S): S {
return GPUStruct.compwise('div', l, r, result);
}
public static cscale<
A extends Uint32Array | Int32Array | Float32Array,
S extends GPUStruct<A>,
>(l: Scalar<A>, r: S, result: S): S {
this.assertUnique(l, r);
this.assertUnique(l, result);
this.assertColocated(l, r, result);
this.assertPackable(4, r, result);
return result.calculate('scale', COMPWISE_SHADER.scale, result === r ? [l] : [l, r], result === r, 4);
}
}

@ -0,0 +1,289 @@
import fs from 'node:fs';
import { Matrix as CPUMatrix, Vector as CPUVector } from '../math';
import { GPUStruct } from './gpu-struct';
import { Matrix } from './matrix';
import { ScalarBatch } from './scalar-batch';
import { VectorBatch } from './vector-batch';
const SHADER_SCALE = fs.readFileSync('shader/bscale.wgsl').toString();
const SHADER_ISUM = fs.readFileSync('shader/isum.wgsl').toString();
const SHADER_MUL = fs.readFileSync('shader/mul.wgsl').toString();
const SHADER_TRANS = fs.readFileSync('shader/transpose.wgsl').toString();
const SHADER_AVG = fs.readFileSync('shader/avg.wgsl').toString();
const SHADER_TOT = fs.readFileSync('shader/tot.wgsl').toString();
export class MatrixBatch<
L extends number,
H extends number,
W extends number,
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>> = NonNullable<ReturnType<A['at']>>,
> extends GPUStruct<A> {
public get width(): W {
return this.size[0] as W;
}
public get height(): H {
return this.size[1] as H;
}
public get layers(): L {
return this.size[2] as L;
}
protected override workgroups(pm: number = 1): [number, number, number] {
return [this.width / GPUStruct.dunit / pm, this.height / GPUStruct.dunit, this.layers / MatrixBatch.lunit];
}
protected override workgroup(): [number, number, number] {
return [GPUStruct.dunit, GPUStruct.dunit, MatrixBatch.lunit];
}
public constructor(device: GPUDevice, array: new (size: number) => A, data: CPUVector<L, CPUMatrix<H, W, T>>);
public constructor(device: GPUDevice, array: new (size: number) => A, l: L, h: H, w: W, data?: CPUVector<L, CPUMatrix<H, W, T>>);
public constructor(device: GPUDevice, array: new (size: number) => A, ...args: unknown[]) {
let [l, h, w]: [number, number, number] = [1, 1, 1];
if (typeof args[0] === 'number') {
[l, h, w] = args as [L, H, W];
} else {
const [data] = args as [CPUVector<L, CPUMatrix<H, W, T>>];
[l, h, w] = [data.length, data[0].length, data[0][0].length, 1];
}
super(device, array, `${h}x${w}[${l}]`, [w, h, l, 1]);
MatrixBatch.assertPackable(1, this);
const data = (typeof args[0] === 'number' ? args[3] : args[0]) as CPUVector<L, CPUMatrix<H, W, T>> | undefined;
if (data) {
this.set(data);
}
}
public index(l: number, y: number, x: number): number {
return (l * this.height + y) * this.width + x;
}
public async set(y: number, x: number, v: T): Promise<void>;
public async set(data: CPUVector<L, CPUMatrix<H, W, T>>): Promise<void>;
public async set(slice: T[][][], ob?: number, oy?: number, ox?: number): Promise<void>;
public async set(m: MatrixBatch<L, H, W, A, T>): Promise<void>;
public async set(...args: unknown[]): Promise<void> {
if (typeof args[0] === 'number') {
const [l, y, x, v]: number[] = args as number[];
if (this._dirty === 'back') {
await this.read();
}
this.data[this.index(l, y, x)] = v;
this._dirty = 'front';
} else if (Array.isArray(args[0])) {
const [data, ol = 0, oy = 0, ox = 0] = args as [T[][][], number | undefined, number | undefined, number | undefined];
if (this._dirty === 'back' && (ol || oy || ox || data.length < this.layers || data[0].length < this.height || data[0][0].length < this.width)) {
await this.read();
}
let l = ol;
let y = oy;
let x = ox;
for (const mat of data ?? []) {
if (l >= this.layers) {
break;
}
for (const row of mat ?? []) {
if (y >= this.height) {
break;
}
for (const v of row) {
if (x >= this.width) {
break;
}
this.data[this.index(l, y, x)] = v;
x++;
}
x = ox;
y++;
}
y = oy;
l++;
}
this._dirty = 'front';
} else {
const [batch] = args as [MatrixBatch<L, H, W, A, T>];
const copy = this._device.createCommandEncoder();
copy.copyBufferToBuffer(batch.buffer(), 0, this.buffer(), 0, this.byteLength);
this._device.queue.submit([copy.finish()]);
this._dirty = 'back';
}
}
public async get(l: number, y: number, x: number): Promise<T>;
public async get(l: number): Promise<CPUMatrix<H, W, T>>;
public async get(): Promise<CPUVector<L, CPUMatrix<H, W, T>>>;
public async get(l?: number, y?: number, x?: number): Promise<T | CPUMatrix<H, W, T> | CPUVector<L, CPUMatrix<H, W, T>>> {
if (this._dirty === 'back') {
await this.read();
}
if (typeof x === 'number') {
return this.data[this.index(l!, y!, x!)] as T;
} else if (typeof l === 'number') {
const result: CPUMatrix<H, W, T> = [] as unknown as CPUMatrix<H, W, T>;
for (let y: number = 0; y < this.height; y++) {
const row: CPUVector<W, T> = [] as unknown as CPUVector<W, T>;
for (let x: number = 0; x < this.width; x++) {
row.push(this.data[this.index(l, y, x)] as T);
}
result.push(row);
}
return result;
} else {
const result: CPUVector<L, CPUMatrix<H, W, T>> = [] as unknown as CPUVector<L, CPUMatrix<H, W, T>>;
for (let l: number = 0; l < this.layers; l++) {
const layer: CPUMatrix<H, W, T> = [] as unknown as CPUMatrix<H, W, T>;
for (let y: number = 0; y < this.height; y++) {
const row: CPUVector<W, T> = [] as unknown as CPUVector<W, T>;
for (let x: number = 0; x < this.width; x++) {
row.push(this.data[this.index(l, y, x)] as T);
}
layer.push(row);
}
result.push(layer);
}
return result;
}
}
protected static override assertPackable(pm: number, ...ms: MatrixBatch<number, number, number, Uint32Array | Int32Array | Float32Array>[]): void {
for (const m of ms) {
if (m.layers % MatrixBatch.lunit !== 0 || m.height % MatrixBatch.dunit !== 0 || m.width % (MatrixBatch.dunit * pm) !== 0) {
throw new Error(`Batch size ${m.height}x${m.width}[${m.layers}] must be divisible by ${MatrixBatch.dunit}x${MatrixBatch.dunit * pm}[${MatrixBatch.lunit}].`);
}
}
}
public static sum<
L extends number,
H extends number,
W extends number,
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>> = NonNullable<ReturnType<A['at']>>,
>(l: MatrixBatch<L, H, W, A, T>, r: MatrixBatch<L, H, W, A, T>, result: MatrixBatch<L, H, W, A, T> = new MatrixBatch(l._device, l._array, l.layers, l.height, l.width)): MatrixBatch<L, H, W, A, T> {
return GPUStruct.csum(l, r, result);
}
public static sub<
L extends number,
H extends number,
W extends number,
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>> = NonNullable<ReturnType<A['at']>>,
>(l: MatrixBatch<L, H, W, A, T>, r: MatrixBatch<L, H, W, A, T>, result: MatrixBatch<L, H, W, A, T> = new MatrixBatch(l._device, l._array, l.layers, l.height, l.width)): MatrixBatch<L, H, W, A, T> {
return GPUStruct.csub(l, r, result);
}
public static isum<
L extends number,
H extends number,
I extends number,
W extends number,
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>>,
>(
lis: VectorBatch<L, I, Int32Array>,
m: MatrixBatch<L, H, W, A, T>,
result: MatrixBatch<L, H, W, A, T> = new MatrixBatch(m._device, m._array, m.layers, m.height, m.width),
): MatrixBatch<L, H, W, A, T> {
this.assertUnique(m, result);
this.assertColocated(m, result);
this.assertPackable(4, m, result);
return result.calculate('isum', SHADER_ISUM, [lis, m], false, 4);
}
public static scale<
L extends number,
H extends number,
W extends number,
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>> = NonNullable<ReturnType<A['at']>>,
>(l: ScalarBatch<L, A, T>, r: MatrixBatch<L, H, W, A, T>, result: MatrixBatch<L, H, W, A, T> = new MatrixBatch(r._device, r._array, r.layers, r.height, r.width)): MatrixBatch<L, H, W, A, T> {
this.assertUnique(l, r);
this.assertUnique(l, result);
this.assertColocated(l, r, result);
this.assertPackable(4, r, result);
return result.calculate('scale', SHADER_SCALE, result === r ? [l] : [l, r], result === r, 4);
}
public static mul<
L extends number,
H extends number,
S extends number,
W extends number,
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>>,
>(
l: MatrixBatch<L, H, S, A, T>,
r: MatrixBatch<L, S, W, A, T>,
result: MatrixBatch<L, H, W, A, T> = new MatrixBatch(l._device, l._array, l.layers, l.height, r.width),
): MatrixBatch<L, H, W, A, T> {
this.assertUnique(l, r, result);
this.assertColocated(l, r, result);
this.assertPackable(1, l, r, result);
return result.calculate('mul', SHADER_MUL, [l, r]);
}
public static transpose<
L extends number,
H extends number,
W extends number,
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>>,
>(
m: MatrixBatch<L, H, W, A, T>,
result: MatrixBatch<L, W, H, A, T> = new MatrixBatch(m._device, m._array, m.layers, m.width, m.height),
): MatrixBatch<L, W, H, A, T> {
this.assertUnique(m, result);
this.assertColocated(m, result);
this.assertPackable(1, m, result);
return result.calculate('transpose', SHADER_TRANS, [m]);
}
public static avg<
L extends number,
H extends number,
W extends number,
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>>,
>(
m: MatrixBatch<L, H, W, A, T>,
result: Matrix<H, W, A, T> = new Matrix(m._device, m._array, m.height, m.width),
): Matrix<H, W, A, T> {
this.assertUnique(m, result);
this.assertColocated(m, result);
this.assertPackable(4, m);
return result.calculate('avg', SHADER_AVG, [m], false, 4);
}
public static tot<
L extends number,
H extends number,
W extends number,
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>>,
>(
m: MatrixBatch<L, H, W, A, T>,
result: Matrix<H, W, A, T> = new Matrix(m._device, m._array, m.height, m.width),
): Matrix<H, W, A, T> {
this.assertUnique(m, result);
this.assertColocated(m, result);
this.assertPackable(4, m);
return result.calculate('tot', SHADER_TOT, [m], false, 4);
}
}

@ -0,0 +1,233 @@
import fs from 'node:fs';
import { Matrix as CPUMatrix, Vector as CPUVector } from '../math';
import { GPUStruct } from './gpu-struct';
import { MatrixBatch } from './matrix-batch';
import { Scalar } from './scalar';
import { Vector } from './vector';
const SHADER_ISUM = fs.readFileSync('shader/isum.wgsl').toString();
const SHADER_MUL = fs.readFileSync('shader/mul.wgsl').toString();
const SHADER_TRANS = fs.readFileSync('shader/transpose.wgsl').toString();
const SHADER_UNAVG = fs.readFileSync('shader/unavg.wgsl').toString();
export class Matrix<
H extends number,
W extends number,
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>> = NonNullable<ReturnType<A['at']>>,
> extends GPUStruct<A> {
public get width(): W {
return this.size[0] as W;
}
public get height(): H {
return this.size[1] as H;
}
protected override workgroups(pm: number = 1): [number, number, number] {
return [this.width / GPUStruct.dunit / pm, this.height / GPUStruct.dunit, 1];
}
protected override workgroup(): [number, number, number] {
return [GPUStruct.dunit, GPUStruct.dunit, 1];
}
public constructor(device: GPUDevice, array: new (size: number) => A, data: CPUMatrix<H, W, T>);
public constructor(device: GPUDevice, array: new (size: number) => A, h: H, w: W, data?: CPUMatrix<H, W, T>);
public constructor(device: GPUDevice, array: new (size: number) => A, ...args: unknown[]) {
let [h, w]: [number, number] = [1, 1];
if (typeof args[0] === 'number') {
[h, w] = args as [H, W];
} else {
const [data] = args as [CPUMatrix<H, W, T>];
[h, w] = [data.length, data[0].length];
}
super(device, array, `${h}x${w}`, [w, h, 1, 1]);
Matrix.assertPackable(1, this);
const data = (typeof args[0] === 'number' ? args[2] : args[0]) as CPUMatrix<H, W, T> | undefined;
if (data) {
this.set(data);
}
}
public index(y: number, x: number): number {
return y * this.width + x;
}
public async set(y: number, x: number, v: T): Promise<void>;
public async set(data: CPUMatrix<H, W, T>): Promise<void>;
public async set(slice: T[][], oy?: number, ox?: number): Promise<void>;
public async set(m: Matrix<H, W, A, T>): Promise<void>;
public async set(...args: unknown[]): Promise<void> {
if (typeof args[0] === 'number') {
const [y, x, v]: number[] = args as number[];
if (this._dirty === 'back') {
await this.read();
}
this.data[this.index(y, x)] = v;
this._dirty = 'front';
} else if (Array.isArray(args[0])) {
const [data, oy = 0, ox = 0] = args as [T[][], number | undefined, number | undefined];
if (this._dirty === 'back' && (oy || ox || data.length < this.height || data[0].length < this.width)) {
await this.read();
}
let y = oy;
let x = ox;
for (const row of data ?? []) {
if (y >= this.height) {
break;
}
for (const v of row) {
if (x >= this.width) {
break;
}
this.data[this.index(y, x)] = v;
x++;
}
x = ox;
y++;
}
this._dirty = 'front';
} else {
const [matrix] = args as [Matrix<H, W, A, T>];
const copy = this._device.createCommandEncoder();
copy.copyBufferToBuffer(matrix.buffer(), 0, this.buffer(), 0, this.byteLength);
this._device.queue.submit([copy.finish()]);
this._dirty = 'back';
}
}
public async get(y: number, x: number): Promise<T>;
public async get(): Promise<CPUMatrix<H, W, T>>;
public async get(y?: number, x?: number): Promise<T | CPUMatrix<H, W, T>> {
if (this._dirty === 'back') {
await this.read();
}
if (typeof x === 'number') {
return this.data[this.index(y!, x!)] as T;
} else {
const result: CPUMatrix<H, W, T> = [] as unknown as CPUMatrix<H, W, T>;
for (let y: number = 0; y < this.height; y++) {
const row: CPUVector<W, T> = [] as unknown as CPUVector<W, T>;
for (let x: number = 0; x < this.width; x++) {
row.push(this.data[this.index(y, x)] as T);
}
result.push(row);
}
return result;
}
}
protected static override assertPackable(pm: number, ...ms: Matrix<number, number, Uint32Array | Int32Array | Float32Array>[]): void {
for (const m of ms) {
if (m.height % Matrix.dunit !== 0 || m.width % (Matrix.dunit * pm) !== 0) {
throw new Error(`Matrix size ${m.height}x${m.width} must be divisible by ${Matrix.dunit}x${Matrix.dunit * pm}.`);
}
}
}
public static sum<
H extends number,
W extends number,
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>> = NonNullable<ReturnType<A['at']>>,
>(l: Matrix<H, W, A, T>, r: Matrix<H, W, A, T>, result: Matrix<H, W, A, T> = new Matrix(l._device, l._array, l.height, l.width)): Matrix<H, W, A, T> {
return GPUStruct.csum(l, r, result);
}
public static sub<
H extends number,
W extends number,
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>> = NonNullable<ReturnType<A['at']>>,
>(l: Matrix<H, W, A, T>, r: Matrix<H, W, A, T>, result: Matrix<H, W, A, T> = new Matrix(l._device, l._array, l.height, l.width)): Matrix<H, W, A, T> {
return GPUStruct.csub(l, r, result);
}
public static isum<
H extends number,
I extends number,
W extends number,
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>>,
>(
lis: Vector<I, Int32Array>,
m: Matrix<H, W, A, T>,
result: Matrix<H, W, A, T> = new Matrix(m._device, m._array, m.height, m.width),
): Matrix<H, W, A, T> {
this.assertUnique(m, result);
this.assertColocated(m, result);
this.assertPackable(4, m, result);
return result.calculate('isum', SHADER_ISUM, [lis, m], false, 4);
}
public static scale<
H extends number,
W extends number,
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>> = NonNullable<ReturnType<A['at']>>,
>(l: Scalar<A, T>, r: Matrix<H, W, A, T>, result: Matrix<H, W, A, T> = new Matrix(r._device, r._array, r.height, r.width)): Matrix<H, W, A, T> {
return GPUStruct.cscale(l, r, result);
}
public static mul<
H extends number,
S extends number,
W extends number,
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>>,
>(
l: Matrix<H, S, A, T>,
r: Matrix<S, W, A, T>,
result: Matrix<H, W, A, T> = new Matrix(l._device, l._array, l.height, r.width),
): Matrix<H, W, A, T> {
this.assertUnique(l, r, result);
this.assertColocated(l, r, result);
this.assertPackable(1, l, r, result);
return result.calculate('mul', SHADER_MUL, [l, r]);
}
public static transpose<
H extends number,
W extends number,
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>>,
>(
m: Matrix<H, W, A, T>,
result: Matrix<W, H, A, T> = new Matrix(m._device, m._array, m.width, m.height),
): Matrix<W, H, A, T> {
this.assertUnique(m, result);
this.assertColocated(m, result);
this.assertPackable(1, m, result);
return result.calculate('transpose', SHADER_TRANS, [m]);
}
public static unavg<
L extends number,
H extends number,
W extends number,
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>>,
>(
l: L,
m: Matrix<H, W, A, T>,
result: MatrixBatch<L, H, W, A, T> = new MatrixBatch(m._device, m._array, l, m.height, m.width),
): MatrixBatch<L, H, W, A, T> {
this.assertUnique(m, result);
this.assertColocated(m, result);
this.assertPackable(4, m);
return result.calculate('unavg', SHADER_UNAVG, [m], false, 4);
}
}

@ -0,0 +1,176 @@
import fs from 'node:fs';
import { Vector as CPUVector } from '../math';
import { GPUStruct } from './gpu-struct';
import { Scalar } from './scalar';
const SHADER_AVG = fs.readFileSync('shader/avg.wgsl').toString();
const SHADER_TOT = fs.readFileSync('shader/tot.wgsl').toString();
export class ScalarBatch<
L extends number,
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>> = NonNullable<ReturnType<A['at']>>,
> extends GPUStruct<A> {
public get layers(): L {
return this.size[2] as L;
}
protected override workgroups(): [number, number, number] {
return [1, 1, this.layers / GPUStruct.lunit];
}
protected override workgroup(): [number, number, number] {
return [1, 1, GPUStruct.lunit];
}
public constructor(device: GPUDevice, array: new (size: number) => A, data: CPUVector<L, T>);
public constructor(device: GPUDevice, array: new (size: number) => A, l: L, data?: CPUVector<L, T>);
public constructor(device: GPUDevice, array: new (size: number) => A, ...args: unknown[]) {
const l: number = typeof args[0] === 'number'
? args[0] as L
: (args[0] as CPUVector<L, T>).length;
super(device, array, `[${l}]`, [1, 1, l, 1]);
ScalarBatch.assertPackable(1, this);
const data = (typeof args[0] === 'number' ? args[1] : args[0]) as CPUVector<L, T> | undefined;
if (data) {
this.set(data);
}
}
public async set(l: number, v: T): Promise<void>;
public async set(data: CPUVector<L, T>): Promise<void>;
public async set(slice: T[], ol?: number): Promise<void>;
public async set(b: ScalarBatch<L, A, T>): Promise<void>;
public async set(...args: unknown[]): Promise<void> {
if (typeof args[0] === 'number') {
const [l, v]: number[] = args as number[];
if (this._dirty === 'back') {
await this.read();
}
this.data[l] = v;
this._dirty = 'front';
} else if (Array.isArray(args[0])) {
const [data, ol = 0] = args as [T[], number | undefined];
if (this._dirty === 'back' && (ol || data.length < this.layers)) {
await this.read();
}
let l = ol;
for (const v of data ?? []) {
if (l >= this.layers) {
break;
}
this.data[l] = v;
l++;
}
this._dirty = 'front';
} else {
const [batch] = args as [ScalarBatch<L, A, T>];
const copy = this._device.createCommandEncoder();
copy.copyBufferToBuffer(batch.buffer(), 0, this.buffer(), 0, this.byteLength);
this._device.queue.submit([copy.finish()]);
this._dirty = 'back';
}
}
public async get(l: number): Promise<T>;
public async get(): Promise<CPUVector<L, T>>;
public async get(l?: number): Promise<T | CPUVector<L, T>> {
if (this._dirty === 'back') {
await this.read();
}
if (typeof l === 'number') {
return this.data[l] as T;
} else {
const result: CPUVector<L, T> = [] as unknown as CPUVector<L, T>;
for (let l: number = 0; l < this.layers; l++) {
result.push(this.data[l] as T);
}
return result;
}
}
protected static override assertPackable(_: number, ...ms: ScalarBatch<number, Uint32Array | Int32Array | Float32Array>[]): void {
for (const m of ms) {
if (m.layers % ScalarBatch.dunit !== 0) {
throw new Error(`Batch size [${m.layers}] must be divisible by [${ScalarBatch.dunit}].`);
}
}
}
public static sum<
L extends number,
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>> = NonNullable<ReturnType<A['at']>>,
>(l: ScalarBatch<L, A, T>, r: ScalarBatch<L, A, T>, result: ScalarBatch<L, A, T> = new ScalarBatch(l._device, l._array, l.layers)): ScalarBatch<L, A, T> {
return GPUStruct.csum(l, r, result);
}
public static sub<
L extends number,
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>> = NonNullable<ReturnType<A['at']>>,
>(l: ScalarBatch<L, A, T>, r: ScalarBatch<L, A, T>, result: ScalarBatch<L, A, T> = new ScalarBatch(l._device, l._array, l.layers)): ScalarBatch<L, A, T> {
return GPUStruct.csub(l, r, result);
}
public static mul<
L extends number,
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>> = NonNullable<ReturnType<A['at']>>,
>(l: ScalarBatch<L, A, T>, r: ScalarBatch<L, A, T>, result: ScalarBatch<L, A, T> = new ScalarBatch(l._device, l._array, l.layers)): ScalarBatch<L, A, T> {
return GPUStruct.cmul(l, r, result);
}
public static div<
L extends number,
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>> = NonNullable<ReturnType<A['at']>>,
>(l: ScalarBatch<L, A, T>, r: ScalarBatch<L, A, T>, result: ScalarBatch<L, A, T> = new ScalarBatch(l._device, l._array, l.layers)): ScalarBatch<L, A, T> {
return GPUStruct.cdiv(l, r, result);
}
public static scale<
L extends number,
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>> = NonNullable<ReturnType<A['at']>>,
>(l: Scalar<A, T>, r: ScalarBatch<L, A, T>, result: ScalarBatch<L, A, T> = new ScalarBatch(r._device, r._array, r.layers)): ScalarBatch<L, A, T> {
return GPUStruct.cscale(l, r, result);
}
public static avg<
L extends number,
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>>,
>(
m: ScalarBatch<L, A, T>,
result: Scalar<A, T> = new Scalar(m._device, m._array),
): Scalar<A, T> {
this.assertUnique(m, result);
this.assertColocated(m, result);
this.assertPackable(4, m);
return result.calculate('avg', SHADER_AVG, [m], false, 4);
}
public static tot<
L extends number,
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>>,
>(
m: ScalarBatch<L, A, T>,
result: Scalar<A, T> = new Scalar(m._device, m._array),
): Scalar<A, T> {
this.assertUnique(m, result);
this.assertColocated(m, result);
this.assertPackable(4, m);
return result.calculate('tot', SHADER_TOT, [m], false, 4);
}
}

@ -0,0 +1,90 @@
import fs from 'node:fs';
import { GPUStruct } from './gpu-struct';
import { ScalarBatch } from './scalar-batch';
const SHADER_UNAVG = fs.readFileSync('shader/unavg.wgsl').toString();
export class Scalar<
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>> = NonNullable<ReturnType<A['at']>>,
> extends GPUStruct<A> {
protected override workgroups(): [number, number, number] {
return [1, 1, 1];
}
protected override workgroup(): [number, number, number] {
return [1, 1, 1];
}
public constructor(device: GPUDevice, array: new (size: number) => A, data?: T) {
super(device, array, 'scalar', [1, 1, 1, 1]);
this.set(data || 0 as T);
}
public async set(v: T | Scalar<A, T>): Promise<void> {
if (typeof v === 'number') {
if (this._dirty === 'back') {
await this.read();
}
this.data[0] = v;
this._dirty = 'front';
} else {
const copy = this._device.createCommandEncoder();
copy.copyBufferToBuffer(v.buffer(), 0, this.buffer(), 0, this.byteLength);
this._device.queue.submit([copy.finish()]);
this._dirty = 'back';
}
}
public async get(): Promise<T> {
if (this._dirty === 'back') {
await this.read();
}
return this.data[0] as T;
}
public static sum<
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>> = NonNullable<ReturnType<A['at']>>,
>(l: Scalar<A, T>, r: Scalar<A, T>, result: Scalar<A, T> = new Scalar(l._device, l._array)): Scalar<A, T> {
return GPUStruct.csum(l, r, result);
}
public static sub<
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>> = NonNullable<ReturnType<A['at']>>,
>(l: Scalar<A, T>, r: Scalar<A, T>, result: Scalar<A, T> = new Scalar(l._device, l._array)): Scalar<A, T> {
return GPUStruct.csub(l, r, result);
}
public static mul<
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>> = NonNullable<ReturnType<A['at']>>,
>(l: Scalar<A, T>, r: Scalar<A, T>, result: Scalar<A, T> = new Scalar(l._device, l._array)): Scalar<A, T> {
return GPUStruct.cmul(l, r, result);
}
public static div<
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>> = NonNullable<ReturnType<A['at']>>,
>(l: Scalar<A, T>, r: Scalar<A, T>, result: Scalar<A, T> = new Scalar(l._device, l._array)): Scalar<A, T> {
return GPUStruct.cdiv(l, r, result);
}
public static unavg<
L extends number,
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>>,
>(
l: L,
m: Scalar<A, T>,
result: ScalarBatch<L, A, T> = new ScalarBatch(m._device, m._array, l),
): ScalarBatch<L, A, T> {
this.assertUnique(m, result);
this.assertColocated(m, result);
this.assertPackable(4, m);
return result.calculate('unavg', SHADER_UNAVG, [m], false, 4);
}
}

@ -0,0 +1,221 @@
import fs from 'node:fs';
import { Vector as CPUVector } from '../math';
import { GPUStruct } from './gpu-struct';
import { ScalarBatch } from './scalar-batch';
import { Vector } from './vector';
const SHADER_AVG = fs.readFileSync('shader/avg.wgsl').toString();
const SHADER_TOT = fs.readFileSync('shader/tot.wgsl').toString();
const SHADER_SCALE = fs.readFileSync('shader/bscale.wgsl').toString();
const SHADER_DOT = fs.readFileSync('shader/mul.wgsl').toString();
export class VectorBatch<
L extends number,
D extends number,
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>> = NonNullable<ReturnType<A['at']>>,
> extends GPUStruct<A> {
public get dims(): D {
return this.size[0] as D;
}
public get layers(): L {
return this.size[2] as L;
}
protected override workgroups(pm: number = 1): [number, number, number] {
return [this.dims / GPUStruct.dunit / pm, 1, this.layers / GPUStruct.lunit];
}
protected override workgroup(): [number, number, number] {
return [GPUStruct.dunit, 1, GPUStruct.lunit];
}
public constructor(device: GPUDevice, array: new (size: number) => A, data: CPUVector<L, CPUVector<D, T>>);
public constructor(device: GPUDevice, array: new (size: number) => A, l: L, d: D, data?: CPUVector<L, CPUVector<D, T>>);
public constructor(device: GPUDevice, array: new (size: number) => A, ...args: unknown[]) {
let [l, d]: [number, number] = [1, 1];
if (typeof args[0] === 'number') {
[l, d] = args as [L, D];
} else {
const [data] = args as [CPUVector<L, CPUVector<D, T>>];
[l, d] = [data.length, data[0].length];
}
super(device, array, `${d}[${l}]`, [d, 1, l, 1]);
VectorBatch.assertPackable(1, this);
const data = (typeof args[0] === 'number' ? args[2] : args[0]) as CPUVector<L, CPUVector<D, T>> | undefined;
if (data) {
this.set(data);
}
}
public index(l: number, i: number): number {
return l * this.dims + i;
}
public async set(l: number, i: number, v: T): Promise<void>;
public async set(data: CPUVector<L, CPUVector<D, T>>): Promise<void>;
public async set(slice: T[][], ol?: number, oi?: number): Promise<void>;
public async set(b: VectorBatch<L, D, A, T>): Promise<void>;
public async set(...args: unknown[]): Promise<void> {
if (typeof args[0] === 'number') {
const [l, i, v]: number[] = args as number[];
if (this._dirty === 'back') {
await this.read();
}
this.data[this.index(l, i)] = v;
this._dirty = 'front';
} else if (Array.isArray(args[0])) {
const [data, ol = 0, oi = 0] = args as [T[][], number | undefined, number | undefined];
if (this._dirty === 'back' && (ol || oi || data.length < this.layers || data[0].length < this.dims)) {
await this.read();
}
let l = ol;
let i = oi;
for (const vec of data ?? []) {
if (l >= this.layers) {
break;
}
for (const v of vec) {
if (i >= this.dims) {
break;
}
this.data[this.index(l, i)] = v;
i++;
}
i = oi;
l++;
}
this._dirty = 'front';
} else {
const [batch] = args as [VectorBatch<L, D, A, T>];
const copy = this._device.createCommandEncoder();
copy.copyBufferToBuffer(batch.buffer(), 0, this.buffer(), 0, this.byteLength);
this._device.queue.submit([copy.finish()]);
this._dirty = 'back';
}
}
public async get(l: number, i: number): Promise<T>;
public async get(l: number): Promise<CPUVector<D, T>>;
public async get(): Promise<CPUVector<L, CPUVector<D, T>>>;
public async get(l?: number, i?: number): Promise<T | CPUVector<D, T> | CPUVector<L, CPUVector<D, T>>> {
if (this._dirty === 'back') {
await this.read();
}
if (typeof i === 'number') {
return this.data[this.index(l!, i!)] as T;
} else if (typeof l === 'number') {
const result: CPUVector<D, T> = [] as unknown as CPUVector<D, T>;
for (let i: number = 0; i < this.dims; i++) {
result.push(this.data[this.index(l, i)] as T);
}
return result;
} else {
const result: CPUVector<L, CPUVector<D, T>> = [] as unknown as CPUVector<L, CPUVector<D, T>>;
for (let l: number = 0; l < this.layers; l++) {
const vec: CPUVector<D, T> = [] as unknown as CPUVector<D, T>;
for (let i: number = 0; i < this.dims; i++) {
vec.push(this.data[this.index(l, i)] as T);
}
result.push(vec);
}
return result;
}
}
protected static override assertPackable(pm: number, ...ms: VectorBatch<number, number, Uint32Array | Int32Array | Float32Array>[]): void {
for (const m of ms) {
if (m.layers % VectorBatch.lunit !== 0 || m.dims % (VectorBatch.dunit * pm) !== 0) {
throw new Error(`Batch size ${m.dims}[${m.layers}] must be divisible by ${VectorBatch.dunit * pm}[${VectorBatch.lunit}].`);
}
}
}
public static sum<
L extends number,
D extends number,
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>> = NonNullable<ReturnType<A['at']>>,
>(l: VectorBatch<L, D, A, T>, r: VectorBatch<L, D, A, T>, result: VectorBatch<L, D, A, T> = new VectorBatch(l._device, l._array, l.layers, l.dims)): VectorBatch<L, D, A, T> {
return GPUStruct.csum(l, r, result);
}
public static sub<
L extends number,
D extends number,
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>> = NonNullable<ReturnType<A['at']>>,
>(l: VectorBatch<L, D, A, T>, r: VectorBatch<L, D, A, T>, result: VectorBatch<L, D, A, T> = new VectorBatch(l._device, l._array, l.layers, l.dims)): VectorBatch<L, D, A, T> {
return GPUStruct.csub(l, r, result);
}
public static mul<
L extends number,
D extends number,
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>> = NonNullable<ReturnType<A['at']>>,
>(l: ScalarBatch<L, A, T>, r: VectorBatch<L, D, A, T>, result: VectorBatch<L, D, A, T> = new VectorBatch(r._device, r._array, r.layers, r.dims)): VectorBatch<L, D, A, T> {
this.assertUnique(l, r);
this.assertUnique(l, result);
this.assertColocated(l, r, result);
this.assertPackable(4, r, result);
return result.calculate('scale', SHADER_SCALE, result === r ? [l] : [l, r], result === r, 4);
}
public static dot<
L extends number,
D extends number,
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>> = NonNullable<ReturnType<A['at']>>,
>(l: VectorBatch<L, D, A, T>, r: VectorBatch<L, D, A, T>, result: ScalarBatch<L, A, T> = new ScalarBatch(l._device, l._array, l.layers)): ScalarBatch<L, A, T> {
throw new Error('Not implemented.');
this.assertUnique(l, r);
this.assertUnique(l, result);
this.assertColocated(l, r, result);
// this.assertPackable(4, r, result);
return result.calculate('dot', SHADER_DOT, [l, r]);
}
public static avg<
L extends number,
D extends number,
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>>,
>(
m: VectorBatch<L, D, A, T>,
result: Vector<D, A, T> = new Vector(m._device, m._array, m.dims),
): Vector<D, A, T> {
this.assertUnique(m, result);
this.assertColocated(m, result);
this.assertPackable(4, m);
return result.calculate('avg', SHADER_AVG, [m], false, 4);
}
public static tot<
L extends number,
D extends number,
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>>,
>(
m: VectorBatch<L, D, A, T>,
result: Vector<D, A, T> = new Vector(m._device, m._array, m.dims),
): Vector<D, A, T> {
this.assertUnique(m, result);
this.assertColocated(m, result);
this.assertPackable(4, m);
return result.calculate('tot', SHADER_TOT, [m], false, 4);
}
}

@ -0,0 +1,164 @@
import fs from 'node:fs';
import { Vector as CPUVector } from '../math';
import { GPUStruct } from './gpu-struct';
import { Scalar } from './scalar';
import { VectorBatch } from './vector-batch';
const SHADER_UNAVG = fs.readFileSync('shader/unavg.wgsl').toString();
const SHADER_DOT = fs.readFileSync('shader/mul.wgsl').toString();
export class Vector<
D extends number,
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>> = NonNullable<ReturnType<A['at']>>,
> extends GPUStruct<A> {
public get dims(): D {
return this.size[0] as D;
}
protected override workgroups(pm: number = 1): [number, number, number] {
return [this.dims / GPUStruct.dunit / pm, 1, 1];
}
protected override workgroup(): [number, number, number] {
return [GPUStruct.dunit, 1, 1];
}
public constructor(device: GPUDevice, array: new (size: number) => A, data: CPUVector<D, T>);
public constructor(device: GPUDevice, array: new (size: number) => A, d: D, data?: CPUVector<D, T>);
public constructor(device: GPUDevice, array: new (size: number) => A, ...args: unknown[]) {
const d: number = typeof args[0] === 'number'
? args[0] as D
: (args[0] as CPUVector<D, T>).length;
super(device, array, `${d}`, [d, 1, 1, 1]);
Vector.assertPackable(1, this);
const data = (typeof args[0] === 'number' ? args[1] : args[0]) as CPUVector<D, T> | undefined;
if (data) {
this.set(data);
}
}
public async set(i: number, v: T): Promise<void>;
public async set(data: CPUVector<D, T>): Promise<void>;
public async set(slice: T[], oi?: number): Promise<void>;
public async set(b: Vector<D, A, T>): Promise<void>;
public async set(...args: unknown[]): Promise<void> {
if (typeof args[0] === 'number') {
const [i, v]: number[] = args as number[];
if (this._dirty === 'back') {
await this.read();
}
this.data[i] = v;
this._dirty = 'front';
} else if (Array.isArray(args[0])) {
const [data, oi = 0] = args as [T[], number | undefined];
if (this._dirty === 'back' && (oi || data.length < this.dims)) {
await this.read();
}
let i = oi;
for (const v of data ?? []) {
if (i >= this.dims) {
break;
}
this.data[i] = v;
i++;
}
this._dirty = 'front';
} else {
const [vector] = args as [Vector<D, A, T>];
const copy = this._device.createCommandEncoder();
copy.copyBufferToBuffer(vector.buffer(), 0, this.buffer(), 0, this.byteLength);
this._device.queue.submit([copy.finish()]);
this._dirty = 'back';
}
}
public async get(i: number): Promise<T>;
public async get(): Promise<CPUVector<D, T>>;
public async get(i?: number): Promise<T | CPUVector<D, T>> {
if (this._dirty === 'back') {
await this.read();
}
if (typeof i === 'number') {
return this.data[i] as T;
} else {
const result: CPUVector<D, T> = [] as unknown as CPUVector<D, T>;
for (let i: number = 0; i < this.dims; i++) {
result.push(this.data[i] as T);
}
return result;
}
}
protected static override assertPackable(pm: number, ...ms: Vector<number, Uint32Array | Int32Array | Float32Array>[]): void {
for (const m of ms) {
if (m.dims % (Vector.dunit * pm) !== 0) {
throw new Error(`Vector size ${m.dims} must be divisible by ${Vector.dunit * pm}.`);
}
}
}
public static sum<
D extends number,
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>> = NonNullable<ReturnType<A['at']>>,
>(l: Vector<D, A, T>, r: Vector<D, A, T>, result: Vector<D, A, T> = new Vector(l._device, l._array, l.dims)): Vector<D, A, T> {
return GPUStruct.csum(l, r, result);
}
public static sub<
D extends number,
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>> = NonNullable<ReturnType<A['at']>>,
>(l: Vector<D, A, T>, r: Vector<D, A, T>, result: Vector<D, A, T> = new Vector(l._device, l._array, l.dims)): Vector<D, A, T> {
return GPUStruct.csub(l, r, result);
}
public static mul<
D extends number,
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>> = NonNullable<ReturnType<A['at']>>,
>(l: Scalar<A, T>, r: Vector<D, A, T>, result: Vector<D, A, T> = new Vector(r._device, r._array, r.dims)): Vector<D, A, T> {
return GPUStruct.cscale(l, r, result);
}
public static dot<
D extends number,
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>> = NonNullable<ReturnType<A['at']>>,
>(l: Vector<D, A, T>, r: Vector<D, A, T>, result: Scalar<A, T> = new Scalar(l._device, l._array)): Scalar<A, T> {
throw new Error('Not implemented.');
this.assertUnique(l, r);
this.assertUnique(l, result);
this.assertColocated(l, r, result);
// this.assertPackable(4, r, result);
return result.calculate('dot', SHADER_DOT, [l, r]);
}
public static unavg<
L extends number,
D extends number,
A extends Uint32Array | Int32Array | Float32Array,
T extends NonNullable<ReturnType<A['at']>>,
>(
l: L,
m: Vector<D, A, T>,
result: VectorBatch<L, D, A, T> = new VectorBatch(m._device, m._array, l, m.dims),
): VectorBatch<L, D, A, T> {
this.assertUnique(m, result);
this.assertColocated(m, result);
this.assertPackable(4, m);
return result.calculate('unavg', SHADER_UNAVG, [m], false, 4);
}
}

@ -0,0 +1,100 @@
import { List, ListItem } from './list';
interface InternalQueueItem<T> {
value: T;
self?: ListItem<InternalQueueItem<T>>;
orphan?: boolean;
}
export interface QueueItem<T> {
readonly value: T;
readonly self: ListItem<QueueItem<T>>;
readonly orphan?: boolean;
}
export abstract class GroupQueueBase<K, T> {
protected _groups: Map<K, List<InternalQueueItem<T>>> = new Map();
protected abstract hash(v: T): K;
public get length(): number {
return this._groups.size;
}
public constructor(values?: Iterable<T>) {
for (const v of values ?? []) {
this.insert(v);
}
}
public insert(value: T): QueueItem<T> {
const hash: K = this.hash(value);
const group: List<InternalQueueItem<T>> = this._groups.get(hash) ?? this._groups.set(hash, new List()).get(hash)!;
const item: InternalQueueItem<T> = { value };
return (item.self = group.append(item)).value as QueueItem<T>;
}
public remove(item: QueueItem<T>): boolean {
if (item.orphan) {
return false;
}
const hash: K = this.hash(item.value);
const group: List<InternalQueueItem<T>> | undefined = this._groups.get(hash);
if (!group) {
return false;
}
group.delete(item.self);
if (!group.length) {
this._groups.delete(hash);
}
return true;
}
public itemOf(value: T): QueueItem<T> | undefined {
const hash: K = this.hash(value);
const group: List<InternalQueueItem<T>> | undefined = this._groups.get(hash);
if (!group) {
return undefined;
}
for (const item of group) {
if (item.value === value) {
return item as QueueItem<T>;
}
}
return undefined;
}
public peek(): List<QueueItem<T>> | undefined {
if (!this._groups.size) {
return undefined;
}
let group: List<InternalQueueItem<T>> | undefined = undefined;
for (const g of this._groups.values()) {
if (g.length > (group?.length ?? 0)) {
group = g;
}
}
return group as List<QueueItem<T>>;
}
public pop(): List<QueueItem<T>> | undefined {
const result: List<QueueItem<T>> | undefined = this.peek();
for (const item of result ?? []) {
this.remove(item);
}
return result;
}
public *[Symbol.iterator](): IterableIterator<List<QueueItem<T>>> {
for (const group of [...this._groups.values()].sort(({ length: ll }, { length: rl }) => rl - ll)) {
yield group as List<QueueItem<T>>;
}
}
}
export class GroupQueue<T> extends GroupQueueBase<T, T> {
protected override hash(v: T): T {
return v;
}
}

@ -0,0 +1,127 @@
interface InternalListItem<T> {
value: T;
prev: InternalListItem<T>;
next: InternalListItem<T>;
orphan?: boolean;
}
export interface ListItem<T> {
readonly value: T;
readonly prev: ListItem<T>;
readonly next: ListItem<T>;
readonly orphan?: boolean;
}
export class List<T> implements Iterable<T> {
protected _head?: InternalListItem<T>;
protected _length: number = 0;
public get first(): T | undefined {
return this._head?.value;
}
public get last(): T | undefined {
return this._head?.prev?.value;
}
public get length(): number {
return this._length;
}
public constructor(values?: Iterable<T>) {
for (const v of values ?? []) {
this.append(v);
}
}
public prepend(value: T): ListItem<T> {
if (!this._head) {
this._head = { value } as InternalListItem<T>;
this._head.prev = this._head.next = this._head;
} else {
this._head = {
value,
next: this._head,
prev: this._head.prev,
};
this._head.prev.next = this._head.next.prev = this._head;
}
this._length++;
return this._head;
}
public append(value: T): ListItem<T> {
this.prepend(value);
this._head = this._head?.next;
return this._head!.prev;
}
public includes(value: T): boolean {
return !!this.itemOf(value);
}
public itemOf(value: T): ListItem<T> | undefined {
for (const item of this.items()) {
if (item.value === value) {
return item;
}
}
return undefined;
}
public set(item: ListItem<T>, value: T): boolean {
if (item.orphan) {
return false;
}
(item as InternalListItem<T>).value = value;
return true;
}
public delete(item: ListItem<T>): boolean {
if (item.orphan) {
return false;
}
if (this._head === item) {
this._head = item.next;
}
if (this._head?.next === this._head) {
this._head = undefined;
}
(item as InternalListItem<T>).prev.next = item.next;
(item as InternalListItem<T>).next.prev = item.prev;
(item as InternalListItem<T>).orphan = true;
this._length--;
return true;
}
public *[Symbol.iterator](): IterableIterator<T> {
for (const item of this.items()) {
yield item.value;
}
}
public values(): IterableIterator<T> {
return this[Symbol.iterator]();
}
public *items(): IterableIterator<ListItem<T>> {
if (!this._head) {
return;
}
let head = this._head;
let item = this._head;
do {
head = this._head;
yield item;
item = item.next;
} while (item !== head);
}
}

@ -0,0 +1,142 @@
export type Vector<D extends number, I = number> = I[] & { length: D };
export type Matrix<R extends number, C extends number, I = number> = Vector<R, Vector<C, I>>;
export function vec<D extends number, I = number>(s: D, f: (i: number) => I): Vector<D, I> {
return Array.from({ length: s }, (_, i) => f(i)) as Vector<D, I>;
}
export function mat<R extends number, C extends number, I = number>(r: R, c: C, f: (x: number, y: number) => number): Matrix<R, C, I> {
return Array.from({ length: r }, (_, y) => Array.from({ length: c }, (_, x) => f(x, y))) as Matrix<R, C, I>;
}
export function sum<R extends number, C extends number>(l: Matrix<R, C>, r: Matrix<R, C>): Matrix<R, C> {
const result: Matrix<R, C> = mat(l.length, l[0].length, () => 0);
for (let i = 0; i < l.length; i++) {
for (let j = 0; j < r[0].length; j++) {
result[i][j] += l[i][j] + r[i][j];
}
}
return result;
}
export function mul<R extends number, C extends number>(l: Matrix<R, C>, r: number): Matrix<R, C>;
export function mul<D0 extends number, D1 extends number>(l: Vector<D0>, r: Matrix<D0, D1>): Vector<D1>;
export function mul<R extends number, S extends number, C extends number>(l: Matrix<R, S>, r: Matrix<S, C>): Matrix<R, C>;
export function mul(l: Matrix<number, number> | Vector<number>, r: Matrix<number, number> | number): Matrix<number, number> | Vector<number> {
if (typeof r === 'number') {
const lm: Matrix<number, number> = l as Matrix<number, number>;
const result = Array.from({ length: lm.length }, () => Array(lm[0].length).fill(0));
for (let i = 0; i < lm.length; i++) {
for (let j = 0; j < lm[i].length; j++) {
result[i][j] += lm[i][j] * r;
}
}
return result;
} else if (!Array.isArray(l[0])) {
const lv: Vector<number> = l as Vector<number>;
const result = Array(r[0].length).fill(0);
for (let j = 0; j < r[0].length; j++) {
for (let i = 0; i < lv.length; i++) {
result[j] += lv[i] * r[i][j];
}
}
return result;
} else {
const lm: Matrix<number, number> = l as Matrix<number, number>;
const result = Array.from({ length: lm.length }, () => Array(r[0].length).fill(0));
for (let i = 0; i < lm.length; i++) {
for (let j = 0; j < r[0].length; j++) {
for (let k = 0; k < r.length; k++) {
result[i][j] += lm[i][k] * r[k][j];
}
}
}
return result;
}
}
export function transpose<R extends number, C extends number>(m: Matrix<R, C>): Matrix<C, R> {
return mat(m[0].length, m.length, (x, y) => m[x][y]);
}
export function softmax(row: number[]): number[] {
const max = Math.max(...row);
const exps = row.map(v => Math.exp(v - max));
const sum = exps.reduce((a, b) => a + b, 0);
return exps.map(v => v / sum);
}
export function msoftmax<R extends number, C extends number>(m: Matrix<R, C>): Matrix<R, C> {
return m.map(softmax) as Matrix<R, C>;
}
export function norm<R extends number, C extends number>(x: Matrix<R, C>, epsilon = 1e-5): Matrix<R, C> {
return x.map(vec => {
const mean = vec.reduce((sum, val) => sum + val, 0) / vec.length;
const variance = vec.reduce((sum, val) => sum + (val - mean) ** 2, 0) / vec.length;
return vec.map(val => (val - mean) / Math.sqrt(variance + epsilon));
}) as Matrix<R, C>;
}
export function relu<D extends number>(vec: Vector<D>): Vector<D> {
return vec.map(v => Math.max(0, v)) as Vector<D>;
}
export function mean<D extends number>(v: Vector<D>): number {
return v.reduce((s, c) => s + c, 0) / v.length;
}
export function drm(data: number[], interval = 0.9): number {
const sorted = [...data].sort((a, b) => a - b);
const count = Math.floor(data.length * interval);
let mi = 0;
let mv = Infinity;
for (let i = 0; i <= data.length - count; i++) {
const range = sorted[i + count - 1] - sorted[i];
if (range < mv) {
mv = range;
mi = i;
}
}
return sorted.slice(mi, mi + count).reduce((a, b) => a + b, 0) / count;
}
export function randseq(n: number): number[] {
const is = Array.from({ length: n }, (_, i) => i);
for (let i = n - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[is[i], is[j]] = [is[j], is[i]]; // swap
}
return is;
}
export function stddev(xs: number[]): number {
const m = mean(xs);
return Math.sqrt(xs.reduce((s, v) => s + (v - m) ** 2, 0) / xs.length);
}
export function ema(xs: number[], alpha = 0.1): number {
let result = xs[0];
for (let i = 1; i < xs.length; i++) {
result = alpha * xs[i] + (1 - alpha) * result;
}
return result;
}
export function slope(xs: number[]): number {
const xMean = (xs.length - 1) / 2;
const yMean = mean(xs);
let num = 0, den = 0;
for (let i = 0; i < xs.length; i++) {
const x = i - xMean;
const y = xs[i] - yMean;
num += x * y;
den += x * x;
}
return num / den;
}

@ -0,0 +1,73 @@
export interface InternalTrieNode {
children: Map<string, InternalTrieNode>;
token?: string;
}
export interface TrieNode {
readonly children: Map<string, TrieNode>;
readonly token?: string;
}
export class Trie implements Iterable<string> {
protected root: InternalTrieNode = { children: new Map() };
constructor(values?: string[]) {
for (const value of values ?? []) {
this.insert(value);
}
}
public insert(value: string) {
let node = this.root;
for (const char of value) {
node = node.children.get(char) ?? node.children.set(char, { children: new Map() }).get(char)!;
}
node.token = value;
}
public match(value: string): string | undefined {
let node: TrieNode = this.root;
let match: string | undefined = undefined;
for (let i = 0; i < value.length && node.children.has(value[i]); i++) {
match = (node = node.children.get(value[i])!).token ?? match;
}
return match;
}
public *[Symbol.iterator](): IterableIterator<string> {
const queue: InternalTrieNode[] = [];
let item: InternalTrieNode | undefined = this.root;
while (item) {
if (item.token) {
yield item.token;
}
queue.push(...item.children.values());
item = queue.pop();
}
}
public toJSON(node: InternalTrieNode = this.root): object {
return [
...node.token ? [node.token] : [],
...[...node.children.entries()].map(([k, c]) => [k, this.toJSON(c)]),
];
}
protected static nodeFromJSON(element: object): InternalTrieNode {
if (!Array.isArray(element) || !element.every(e => typeof e === 'string'
|| Array.isArray(e) && e.length === 2)) {
throw new Error('Malformed Trie JSON.');
}
return {
children: new Map((typeof element[0] === 'string' ? element.slice(1) : element).map(([k, v]) => [k, this.nodeFromJSON(v)])),
token: typeof element[0] === 'string' ? element[0] : undefined,
};
}
public static fromJSON(json: object): Trie {
const result = new Trie();
result.root = Trie.nodeFromJSON(json);
return result;
}
}

@ -0,0 +1,196 @@
import fs from 'node:fs';
import { create, globals } from 'webgpu';
import yargs from 'yargs';
import { Matrix } from './data/gpu/matrix';
import { Scalar } from './data/gpu/scalar';
import { mul, sum, transpose } from './data/math';
import { Model } from './model-gpu';
import { Tokenizer } from './tokenizer';
Object.assign(globalThis, globals);
const GPU = create([]);
const device = await GPU.requestAdapter({ powerPreference: 'high-performance' }).then(adapter => adapter?.requestDevice());
if (!device) {
process.stderr.write('Can not find device.\n');
process.exit(1);
}
process.stderr.write(`Running on: ${device.adapterInfo.description}.\n`);
const args = await yargs()
.option('model', { type: 'string', demandOption: true })
.command('test-matrix', 'Test core components')
.command('train-tokenizer', 'Train new tokenizer', yargs => yargs
.option('vocab', { type: 'number', demandOption: true }))
.command('tokenize', 'Tokenize input using existing tokenizer', yargs => yargs
.option('input', { type: 'string' })
.option('output', { type: 'string' }))
.command('configure', 'Configure new model', yargs => yargs
.option('window', { type: 'number', demandOption: true })
.option('dimensions', { type: 'number', demandOption: true })
.option('heads', { type: 'number', demandOption: true })
.option('ffs', { type: 'number', demandOption: true }))
.command('train', 'Train existing model', yargs => yargs
.option('epochs', { type: 'number' })
.option('stats', { type: 'string' })
.option('logits', { type: 'string' })
.option('samples', { type: 'string' }))
.command('transform', 'Run single step of existing model', yargs => yargs
.option('input', { type: 'string' })
.option('output', { type: 'string' })
.option('temperature', { type: 'number' })
.option('logits', { type: 'string' }))
.command('run', 'Run existing model', yargs => yargs
.option('input', { type: 'string' })
.option('output', { type: 'string' })
.option('temperature', { type: 'number' })
.option('samples', { type: 'string' })
.option('logits', { type: 'string' }))
.command('benchmark', 'Benchmark existing model')
.demandCommand(1)
.parse(process.argv.slice(2));
if (args._.includes('test-matrix')) {
(Matrix as any).unit = 1;
const SL = new Matrix(device, Float32Array, 2, 4, [
[3, -7, 6, 12],
[11, -5, 7, 3],
]);
const SR = new Matrix(device, Float32Array, 2, 4, [
[6, 12, 3, -7],
[7, 3, 11, -5],
]);
const SO = new Matrix(device, Float32Array, 2, 4);
const SS = new Scalar(device, Float32Array, 3);
const ML = new Matrix(device, Float32Array, 2, 6, [
[3, -7, 6, 12, 4, 2],
[11, -5, 7, 3, 2, 4],
]);
const MR = new Matrix(device, Float32Array, 6, 2, [
[6, 12],
[3, -7],
[7, 3],
[11, -5],
[4, 2],
[2, 4],
]);
const MO = new Matrix(device, Float32Array, 2, 2);
console.log('Sum: CPU: ', sum(await SL.get(), await SR.get()));
console.log('Sum: GPU (new): ', await Matrix.sum(SL, SR, SO).get());
console.log('Sum: GPU (in-place): ', await Matrix.sum(SL, SR, SR).get());
console.log('Scale: CPU: ', mul(await SL.get(), 3));
console.log('Scale: GPU (new): ', await Matrix.scale(SS, SL, SR).get());
console.log('Scale: GPU (in-place): ', await Matrix.scale(SS, SL, SL).get());
console.log('Mul: CPU: ', mul(await ML.get(), await MR.get()));
console.log('Mul: GPU: ', await Matrix.mul(ML, MR, MO).get());
console.log('Transpose: CPU: ', transpose(await ML.get()));
console.log('Transpose: GPU: ', await Matrix.transpose(ML, MR).get());
}
const dir: string = args.model;
let tokenizer: Tokenizer | undefined;
let model: Model | undefined;
if (args._.includes('train-tokenizer')) {
tokenizer = new Tokenizer(args['vocab'] as number, fs.readFileSync(`${dir}/corpus.txt`).toString());
fs.writeFileSync(`${dir}/vocab.json`, JSON.stringify(tokenizer.toJSON()));
fs.writeFileSync(`${dir}/vocab-flat.txt`, tokenizer.tokens.map(s => JSON.stringify(s).slice(1, -1)).join('\n'));
}
if (args._.includes('tokenize') || args._.includes('configure') || args._.includes('train') || args._.includes('transform') || args._.includes('run') || args._.includes('benchmark')) {
if (!tokenizer && !fs.existsSync(`${dir}/vocab.json`)) {
process.stderr.write('Can not find tokenizer data.\n');
process.exit(1);
}
tokenizer ??= new Tokenizer(JSON.parse(fs.readFileSync(`${dir}/vocab.json`).toString()));
if (args._.includes('tokenize')) {
fs.writeFileSync(args['output'] as string || 1, tokenizer.detokenize(tokenizer.tokenize(fs.readFileSync(args['input'] as string || 0).toString())).map(s => JSON.stringify(s).slice(1, -1)).join('\n'));
}
if (args._.includes('configure')) {
model = new Model(device, tokenizer, {
tokenizer: `${tokenizer.tokens.length}${tokenizer.tokens[0]}`,
vs: tokenizer.tokens.length,
ws: args['window'] as number,
ds: args['dimensions'] as number,
});
fs.writeFileSync(`${dir}/model.json`, JSON.stringify(await model.toJSON()));
if (args['stats']) { fs.writeFileSync(args['stats'] as string, 'index,timestamp,epoch,batch,lr,loss,accuracy,grad_inp,grad_qry,grad_key,grad_val,grad_out,queueing,computing,inferring\n'); }
if (args['logits']) { fs.writeFileSync(args['logits'] as string, 'sample,token,itid,ttid,t0,o0,p0,t1,o1,p1,t2,o2,p2,tNm2,oNm2,pNm2,tNm1,oNm1,pNm1,tN,oN,pN,sum,stddev,entropy,loss\n'); }
if (args['samples']) { fs.writeFileSync(args['samples'] as string, ''); }
}
if (args._.includes('train') || args._.includes('transform') || args._.includes('run') || args._.includes('benchmark')) {
if (!model && !fs.existsSync(`${dir}/model.json`)) {
process.stderr.write('Can not find model data.\n');
process.exit(1);
}
model ??= new Model(device, tokenizer, JSON.parse(fs.readFileSync(`${dir}/model.json`).toString()));
if (args._.includes('train')) {
const snapshot = (json: object) => fs.writeFileSync(`${dir}/model.json`, JSON.stringify(json));
await model.train(
args.epochs as number || 'auto',
fs.readFileSync(`${dir}/corpus.txt`).toString(),
{
logs: process.stderr,
stats: args['stats'] ? fs.createWriteStream(args['stats'] as string, { flags: 'a' }) : undefined,
logits: args['logits'] ? fs.createWriteStream(args['logits'] as string, { flags: 'a' }) : undefined,
samples: args['samples'] ? fs.createWriteStream(args['samples'] as string, { flags: 'a' }) : undefined,
snapshot,
});
snapshot(await model.toJSON());
}
if (args._.includes('transform')) {
const [inp, out] = await model.transform(
tokenizer.tokenize(fs.readFileSync(args['input'] as string || 0).toString()),
{
temp: args['temperature'] as number || 1,
logits: args['logits'] ? fs.createWriteStream(args['logits'] as string) : undefined,
},
);
fs.writeFileSync(args['output'] as string || 1, `Input:\n${tokenizer.detokenize(inp).join('')}\nOutput:${tokenizer.detokenize(out).join('\n')}\n`);
}
if (args._.includes('run')) {
fs.writeFileSync(args['output'] as string || 1, tokenizer.detokenize(await model.run(
tokenizer.tokenize(fs.readFileSync(args['input'] as string || 0).toString()),
{
temp: args['temperature'] as number || 1,
logits: args['logits'] ? fs.createWriteStream(args['logits'] as string) : undefined,
samples: args['samples'] as number || model.ws,
},
)).join('') + '\n');
}
if (args._.includes('benchmark')) {
throw new Error('Not implemented.');
}
}
}
await device.queue.onSubmittedWorkDone();
device.destroy();
process.stderr.write('All done!\n');

@ -0,0 +1,261 @@
import { Matrix, Vector, mat, mul, softmax, sum, transpose } from './data/math';
import { TID } from './tokenizer';
export interface ModelInitData<
V extends number,
W extends number,
D extends number,
H extends number,
F extends number,
> {
readonly vs: V;
readonly ws: W;
readonly ds: D;
readonly hs: H;
readonly fs: F;
}
export interface ModelData<
V extends number,
W extends number,
D extends number,
H extends number,
HD extends number,
F extends number,
FD extends number,
> extends ModelInitData<V, W, D, H, F> {
readonly trained: true;
readonly positions: Matrix<W, D>;
readonly embeddings: Matrix<V, D>;
readonly weights: {
query: Matrix<D, HD>;
key: Matrix<D, HD>;
value: Matrix<D, HD>;
attn: Matrix<D, D>;
ffi: Matrix<D, FD>;
ffo: Matrix<FD, D>;
out: Matrix<D, V>;
};
}
const ES: number = 1;
export class Model<
V extends number = number,
W extends number = number,
D extends number = number,
H extends number = number,
HD extends number = number,
F extends number = number,
FD extends number = number,
> {
protected readonly vs: V;
protected readonly ws: W;
protected readonly ds: D;
protected readonly hs: H;
protected readonly hds: HD;
protected readonly fs: F;
protected readonly fds: FD;
protected positions: Matrix<W, D>;
protected embeddings: Matrix<V, D>;
protected readonly weights: {
query: Matrix<D, HD>;
key: Matrix<D, HD>;
value: Matrix<D, HD>;
attn: Matrix<D, D>;
ffi: Matrix<D, FD>;
ffo: Matrix<FD, D>;
out: Matrix<D, V>;
};
protected embed(tids: Vector<W, TID>): Matrix<W, D> {
return sum(tids.map(tid => this.embeddings[tid]) as Matrix<W, D>, this.positions);
}
// protected attentize<N extends number>(input: Matrix<N, D>): Matrix<N, D> {
// const SCALE: number = 1 / Math.sqrt(this.hds);
// const Q: Matrix<N, HD> = mul(input, this.weights.query);
// const K: Matrix<N, HD> = mul(input, this.weights.key);
// const V: Matrix<N, HD> = mul(input, this.weights.value);
// const scores: Matrix<N, N> = msoftmax(mul(mul(Q, transpose(K)), SCALE));
// const out: Matrix<N, HD> = mul(scores, V);
// return mul(out as unknown as Matrix<N, D>, this.weights.attn);
// }
// protected feedforward<N extends number>(input: Matrix<N, D>): Matrix<N, D> {
// return input.map((v: Vector<D>) => mul(relu(mul(v, this.weights.ffi)), this.weights.ffo)) as Matrix<N, D>;
// }
// protected transform<N extends number>(input: Matrix<N, D>): Matrix<N, D> {
// const attned: Matrix<N, D> = sum(input, this.attentize(input));
// const ffned: Matrix<N, D> = sum(input, this.feedforward(attned));
// return ffned;
// }
protected logitize(input: Matrix<W, D>): Matrix<W, V> {
return mul(input, this.weights.out).map(softmax) as Matrix<W, V>;
}
protected argmax(logit: Vector<V>): TID {
return logit.reduce((mi, p, i, a) => p > a[mi] ? i : mi, 0);
}
protected sample(logit: Vector<V>): TID {
let p: number = 0;
const t: number = Math.random();
for (let i = 0; i < logit.length; i++) {
p += logit[i];
if (t < p) {
return i;
}
}
return 0;
}
protected forward(tids: Vector<W, TID>): Matrix<W, V> {
return this.logitize(this.embed(tids));
}
protected loss<N extends number>(probs: Vector<N, Vector<V>>, target: Vector<N, TID>): number {
return probs.reduce((l: number, ps: Vector<V>, i: number) => l - Math.log(ps[target[i]] + 1e-10), 0) / probs.length;
}
protected grad(probs: Matrix<W, V>, target: Vector<W, TID>): Matrix<W, V> {
const grads = mul(probs, 1);
target.forEach((t, i) => grads[i][t] -= 1);
return grads;
}
protected updateWOut<N extends number>(input: Matrix<N, D>, grad: Matrix<N, V>, lr: number): void {
this.weights.out = sum(mul(mul(transpose(input), grad), -lr), this.weights.out);
}
public constructor(_device: unknown, data: ModelInitData<V, W, D, H, F> | ModelData<V, W, D, H, HD, F, FD>) {
this.vs = data.vs;
this.ws = data.ws;
this.ds = data.ds;
this.hs = data.hs;
this.hds = (this.ds / this.hs) as HD;
this.fs = data.fs;
this.fds = (this.ds * this.fs) as FD;
if (!('trained' in data)) {
this.positions = mat(this.ws, this.ds, (d, i) => {
const angle = i / Math.pow(10000, 2 * Math.floor(d / 2) / this.ds);
return d % 2 === 0 ? Math.sin(angle) * ES : Math.cos(angle) * ES;
});
this.embeddings = mat(this.vs, this.ds, () => Math.random() * 2 * ES - ES);
this.weights = {
query: mat(this.ds, this.hds, () => Math.random() * 0.02 - 0.01),
key: mat(this.ds, this.hds, () => Math.random() * 0.02 - 0.01),
value: mat(this.ds, this.hds, () => Math.random() * 0.02 - 0.01),
attn: mat(this.ds, this.ds, () => Math.random() * 0.02 - 0.01),
ffi: mat(this.ds, this.fds, () => Math.random() * 0.02 - 0.01),
ffo: mat(this.fds, this.ds, () => Math.random() * 0.02 - 0.01),
out: mat(this.ds, this.vs, () => Math.random() * 0.2 - 0.1),
};
} else {
this.positions = data.positions;
this.embeddings = data.embeddings;
this.weights = data.weights;
}
}
public train(data: TID[], epochs: number): void {
let log = Date.now();
const samples: number[] = [];
console.log('Training model.');
const runs = data.length - this.ws - 1;
for (let epoch = 0; epoch < epochs; epoch++) {
for (let s = 0; s < runs; s++) {
const begin: number = Date.now();
const source = data.slice(s, s + this.ws) as Vector<W>;
const target = data.slice(s + 1, s + 1 + this.ws) as Vector<W>;
// const encoded = transform(model, embed(model, source));
const embedded = this.embed(source);
const logits = this.logitize(embedded);
this.updateWOut(embedded, this.grad(logits, target), 0.001);
const output = logits.map(this.sample);
const a = 100 * target.filter((t, i) => output[i] === t).length / target.length;
const l = this.loss(logits, target);
const end: number = Date.now();
samples.push(end - begin);
if ((epoch * runs + s) % Math.round(runs * epochs / Math.min(100, epochs)) === 0 || epoch === epochs - 1 && s === runs - 1 || end - log > 5000) {
console.log(`Done ${samples.length} / ${epochs * runs}, accuracy ${a.toFixed(0)}%, loss ${l}, in ${end - begin}ms, ETA ${Math.round(samples.reduce((a, b) => a + b, 0) / samples.length * (epochs * runs - samples.length) / 1000)}s`);
log = end;
}
}
}
}
public run(tids: TID[], samples: number = this.ws): TID[] {
const inputs: Vector<W> = [
...Array(Math.max(0, this.ws - tids.length)).fill(0),
...tids.slice(-this.ws)] as Vector<W>;
const result: Vector<W, TID> = [] as unknown as Vector<W, TID>;
while (result.length < samples) {
const next: TID = this.sample(this.forward(inputs)[this.ws - 1]);
result.push(next);
inputs.shift();
inputs.push(next);
}
return result;
}
public benchmark(data: TID[]): void {
let log = Date.now();
const samples: number[] = [];
console.log('Testing model.');
let accuracy: number = 0;
let loss: number = 0;
const runs = data.length - this.ws - 1;
for (let s = 0; s < runs; s++) {
const begin: number = Date.now();
const source = data.slice(s, s + this.ws) as Vector<W>;
const target = data.slice(s + 1, s + 1 + this.ws) as Vector<W>;
const embedded = this.embed(source);
const logits = this.logitize(embedded);
const output = logits.map(this.sample);
const a = 100 * target.filter((t, i) => output[i] === t).length / target.length;
const l = this.loss(logits, target);
accuracy += a / runs;
loss += l / runs;
const end: number = Date.now();
samples.push(end - begin);
if (end - log > 5000 || s === runs - 1) {
console.log(`Done ${samples.length} / ${runs}, accuracy ${a.toFixed(0)}% / ${accuracy.toFixed()}%, loss ${l.toFixed(4)} / ${loss.toFixed(4)}, in ${end - begin}ms, ETA ${Math.round(samples.reduce((a, b) => a + b, 0) / samples.length * (runs - samples.length) / 1000)}s`);
log = end;
}
}
}
public toJSON(): ModelData<V, W, D, H, HD, F, FD> {
return {
vs: this.vs,
ws: this.ws,
ds: this.ds,
hs: this.hs,
fs: this.fs,
trained: true,
positions: this.positions,
embeddings: this.embeddings,
weights: this.weights,
};
}
}

@ -0,0 +1,662 @@
import fs from 'node:fs';
import { Writable } from 'node:stream';
import { Matrix } from './data/gpu/matrix';
import { Scalar } from './data/gpu/scalar';
import { Vector } from './data/gpu/vector';
import { Matrix as CPUMatrix, Vector as CPUVector, drm, mat, randseq, slope, stddev, vec } from './data/math';
import { TID, Tokenizer } from './tokenizer';
const SHADER_HOTONES = fs.readFileSync('shader/hotones.wgsl').toString();
const SHADER_EMBED = fs.readFileSync('shader/embed.wgsl').toString();
const SHADER_ATTW = fs.readFileSync('shader/attw.wgsl').toString();
const SHADER_SOFTMAX_EXP = fs.readFileSync('shader/softmaxexp.wgsl').toString();
const SHADER_SOFTMAX_NORM = fs.readFileSync('shader/softmaxnorm.wgsl').toString();
const SHADER_DLDO = fs.readFileSync('shader/dldo.wgsl').toString();
const SHADER_OTDLDO = fs.readFileSync('shader/otdldo.wgsl').toString();
const SHADER_TOTDLDOWA = fs.readFileSync('shader/totdldowa.wgsl').toString();
const SHADER_DLDS = fs.readFileSync('shader/dlds.wgsl').toString();
const SHADER_GRAD_TOUT = fs.readFileSync('shader/gradtout.wgsl').toString();
const SHADER_GRAD_TATTW = fs.readFileSync('shader/gradtattw.wgsl').toString();
const SHADER_GRAD_INP = fs.readFileSync('shader/gradinp.wgsl').toString();
const SHADER_ARGMAX = fs.readFileSync('shader/argmax.wgsl').toString();
const SHADER_SAMPLES = fs.readFileSync('shader/samples.wgsl').toString();
export interface ModelInitData<
V extends number,
W extends number,
D extends number,
> {
readonly tokenizer: string;
readonly vs: V;
readonly ws: W;
readonly ds: D;
}
export interface ModelData<
V extends number,
W extends number,
D extends number,
> extends ModelInitData<V, W, D> {
stage: 'early' | 'mid' | 'end';
tokenizer: string;
samples: number;
weights: {
inp: CPUMatrix<V, D>;
pos: CPUMatrix<W, D>;
tqry: CPUMatrix<D, D>;
tkey: CPUMatrix<D, D>;
tval: CPUMatrix<D, D>;
out: CPUMatrix<V, D>;
};
}
interface TrainingPoint {
index: number;
timestamp: number;
epoch: number;
batch: number;
lr: number;
loss: number;
ginp: number;
gqry: number;
gkey: number;
gval: number;
gout: number;
accuracy: number;
queueing: number;
computing: number;
inferring: number;
}
interface LogitsPoint {
sample: number;
token: number;
itid: string;
ttid: string;
t0: string;
o0: number;
p0: number;
t1: string;
o1: number;
p1: number;
t2: string;
o2: number;
p2: number;
tNm2: string;
oNm2: number;
pNm2: number;
tNm1: string;
oNm1: number;
pNm1: number;
tN: string;
oN: number;
pN: number;
sum: number;
stddev: number;
entropy: number;
loss: number;
}
export class Model<
V extends number = number,
W extends number = number,
D extends number = number,
> {
protected readonly device: GPUDevice;
public readonly tokenizer: Tokenizer;
public readonly vs: V;
public readonly ws: W;
public readonly ds: D;
protected samples: number = 0;
protected stage: 'early' | 'mid' | 'end' = 'early';
protected readonly weights: {
readonly inp: Matrix<V, D, Float32Array>;
readonly pos: Matrix<W, D, Float32Array>;
readonly tqry: Matrix<D, D, Float32Array>;
readonly tkey: Matrix<D, D, Float32Array>;
readonly tval: Matrix<D, D, Float32Array>;
readonly tout: Matrix<V, D, Float32Array>;
};
protected readonly processing: {
readonly ds: Scalar<Float32Array>;
readonly lr: Scalar<Float32Array>;
readonly itids: Vector<W, Int32Array, TID>;
readonly ilogits: Matrix<W, V, Float32Array>;
readonly e: Matrix<W, D, Float32Array>;
readonly q: Matrix<W, D, Float32Array>;
readonly k: Matrix<W, D, Float32Array>;
readonly tk: Matrix<D, W, Float32Array>;
readonly qtk: Matrix<W, W, Float32Array>;
readonly v: Matrix<W, D, Float32Array>;
readonly s: Matrix<W, W, Float32Array>;
readonly se: Matrix<W, W, Float32Array>;
readonly wa: Matrix<W, W, Float32Array>;
readonly a: Matrix<W, D, Float32Array>;
readonly o: Matrix<W, V, Float32Array>;
readonly temp: Scalar<Float32Array>;
readonly tempo: Matrix<W, V, Float32Array>;
readonly on: Matrix<W, V, Float32Array>;
readonly ologits: Matrix<W, V, Float32Array>;
readonly dldo: Matrix<W, V, Float32Array>;
readonly otdldo: Matrix<D, W, Float32Array>;
readonly totdldowa: Matrix<W, D, Float32Array>;
readonly dlds: Matrix<W, W, Float32Array>;
readonly dldsk: Matrix<W, D, Float32Array>;
readonly dldsq: Matrix<W, D, Float32Array>;
readonly ginp: Matrix<V, D, Float32Array>;
readonly gtqry: Matrix<D, D, Float32Array>;
readonly gtkey: Matrix<D, D, Float32Array>;
readonly gtval: Matrix<D, D, Float32Array>;
readonly gtout: Matrix<V, D, Float32Array>;
readonly rnd: Vector<W, Float32Array>;
readonly otids: Vector<W, Int32Array, TID>;
readonly ttids: Vector<W, Int32Array, TID>;
};
protected softmax<H extends number, W extends number>(
m: Matrix<H, W, Float32Array>,
e: Matrix<H, W, Float32Array> = new Matrix(this.device, Float32Array, m.height, m.width),
r: Matrix<H, W, Float32Array> = new Matrix(this.device, Float32Array, m.height, m.width),
): Matrix<H, W, Float32Array> {
e.calculate('softmaxexp', SHADER_SOFTMAX_EXP, [m], false, 4);
r.calculate('softmaxnorm', SHADER_SOFTMAX_NORM, [e], false, 4);
return r;
}
protected async ilogits(itids: Vector<W, Int32Array, TID> = this.processing.itids): Promise<Matrix<W, V, Float32Array>> {
this.processing.ilogits.calculate('hotones', SHADER_HOTONES, [itids]);
return this.processing.ilogits;
}
protected async e(ilogits: Matrix<W, V, Float32Array> = this.processing.ilogits): Promise<Matrix<W, D, Float32Array>> {
this.processing.e.calculate('embed', SHADER_EMBED, [ilogits, this.weights.inp, this.weights.pos]);
return this.processing.e;
}
protected q(e: Matrix<W, D, Float32Array> = this.processing.e): Matrix<W, D, Float32Array> {
this.processing.q.calculate('attw', SHADER_ATTW, [e, this.weights.tqry]);
return this.processing.q;
}
protected k(e: Matrix<W, D, Float32Array> = this.processing.e): Matrix<W, D, Float32Array> {
this.processing.k.calculate('attw', SHADER_ATTW, [e, this.weights.tkey]);
return this.processing.k;
}
protected v(e: Matrix<W, D, Float32Array> = this.processing.e): Matrix<W, D, Float32Array> {
this.processing.v.calculate('attw', SHADER_ATTW, [e, this.weights.tval]);
return this.processing.v;
}
protected s(
q: Matrix<W, D, Float32Array> = this.processing.q,
k: Matrix<W, D, Float32Array> = this.processing.k,
): Matrix<W, W, Float32Array> {
Matrix.transpose(k, this.processing.tk);
Matrix.mul(q, this.processing.tk, this.processing.qtk);
Matrix.scale(this.processing.ds, this.processing.qtk, this.processing.s);
return this.processing.s;
}
protected async wa(s: Matrix<W, W, Float32Array> = this.processing.s): Promise<Matrix<W, W, Float32Array>> {
this.softmax(s, this.processing.se, this.processing.wa);
return this.processing.wa;
}
protected a(
wa: Matrix<W, W, Float32Array> = this.processing.wa,
v: Matrix<W, D, Float32Array> = this.processing.v,
): Matrix<W, D, Float32Array> {
Matrix.mul(wa, v, this.processing.a);
return this.processing.a;
}
protected o(a: Matrix<W, D, Float32Array> = this.processing.a): Matrix<W, V, Float32Array> {
this.processing.o.calculate('attw', SHADER_ATTW, [a, this.weights.tout]);
return this.processing.o;
}
protected async ologits(o: Matrix<W, V, Float32Array> = this.processing.o, temp: number = 1): Promise<Matrix<W, V, Float32Array>> {
await this.processing.temp.set(temp);
Matrix.scale(this.processing.temp, o, this.processing.tempo);
this.softmax(this.processing.tempo, this.processing.on, this.processing.ologits);
return this.processing.ologits;
}
protected async rnd(): Promise<Vector<W, Float32Array>> {
await this.processing.rnd.set(vec(this.ws, () => Math.random()));
return this.processing.rnd;
}
protected otids(): Vector<W, Int32Array, TID>;
protected otids(ologits: Matrix<W, V, Float32Array>): Vector<W, Int32Array, TID>;
protected otids(rnd: Vector<W, Float32Array>, ologits: Matrix<W, V, Float32Array>): Vector<W, Int32Array, TID>;
protected otids(...args: unknown[]): Vector<W, Int32Array, TID> {
if (args.length === 1) {
const [ologits = this.processing.ologits] = args as [Matrix<W, V, Float32Array>];
this.processing.otids.calculate('argmax', SHADER_ARGMAX, [ologits]);
return this.processing.otids;
} else {
const [rnd = this.processing.rnd, ologits = this.processing.ologits] = args as [Vector<W, Float32Array> | undefined, Matrix<W, V, Float32Array> | undefined];
this.processing.otids.calculate('samples', SHADER_SAMPLES, [ologits, rnd]);
return this.processing.otids;
}
}
protected loss<N extends number>(probs: CPUVector<N, CPUVector<V>>, target: CPUVector<N, TID>): number {
return probs.reduce((l: number, ps: CPUVector<V>, i: number) => l - Math.log(ps[target[i]] + 1e-10), 0) / probs.length;
}
protected async upd(
ilogits: Matrix<W, V, Float32Array> = this.processing.ilogits,
e: Matrix<W, D, Float32Array> = this.processing.e,
q: Matrix<W, D, Float32Array> = this.processing.q,
k: Matrix<W, D, Float32Array> = this.processing.k,
v: Matrix<W, D, Float32Array> = this.processing.v,
wa: Matrix<W, W, Float32Array> = this.processing.wa,
a: Matrix<W, D, Float32Array> = this.processing.a,
ologits: Matrix<W, V, Float32Array> = this.processing.ologits,
ttids: Vector<W, Int32Array, TID> = this.processing.ttids,
lr: number = 0.001,
): Promise<void> {
await this.processing.lr.set(-lr);
this.processing.dldo.calculate('dldo', SHADER_DLDO, [
ologits,
ttids,
this.processing.lr,
]);
this.processing.otdldo.calculate('otdldo', SHADER_OTDLDO, [this.processing.dldo, this.weights.tout]);
this.processing.dlds.calculate('dlds', SHADER_DLDS, [
this.processing.ds,
wa,
v,
this.processing.otdldo,
]);
Matrix.mul(this.processing.dlds, k, this.processing.dldsk);
Matrix.mul(Matrix.transpose(this.processing.dlds), q, this.processing.dldsq);
this.processing.ginp.calculate('gradinp', SHADER_GRAD_INP, [
ilogits,
this.processing.dldsq,
this.processing.dldsk,
this.processing.totdldowa,
this.weights.tqry,
this.weights.tkey,
this.weights.tval,
]);
Matrix.sum(this.processing.ginp, this.weights.inp, this.weights.inp);
this.processing.gtout.calculate('gradtout', SHADER_GRAD_TOUT, [a, this.processing.dldo]);
Matrix.sum(this.processing.gtout, this.weights.tout, this.weights.tout);
this.processing.gtqry.calculate('gradtqry', SHADER_GRAD_TATTW, [e, this.processing.dldsk]);
Matrix.sum(this.processing.gtqry, this.weights.tqry, this.weights.tqry);
this.processing.gtkey.calculate('gradtkey', SHADER_GRAD_TATTW, [e, this.processing.dldsq]);
Matrix.sum(this.processing.gtkey, this.weights.tkey, this.weights.tkey);
this.processing.totdldowa.calculate('totdldowa', SHADER_TOTDLDOWA, [wa, this.processing.otdldo]);
this.processing.gtval.calculate('gradtval', SHADER_GRAD_TATTW, [e, this.processing.totdldowa]);
Matrix.sum(this.processing.gtval, this.weights.tval, this.weights.tval);
}
public constructor(
device: GPUDevice,
tokenizer: Tokenizer,
data: ModelInitData<V, W, D> | ModelData<V, W, D>,
) {
this.device = device;
this.tokenizer = tokenizer;
this.vs = data.vs;
this.ws = data.ws;
this.ds = data.ds;
if (!('stage' in data)) {
this.weights = {
inp: new Matrix(this.device, Float32Array, mat(this.vs, this.ds, () => (Math.random() - 0.5) * Math.sqrt(1 / this.ds))),
pos: new Matrix(this.device, Float32Array, mat(this.ws, this.ds, (i, d) => {
const angle = i / Math.pow(10000, 2 * Math.floor(d / 2) / this.ds);
return (d % 2 === 0 ? Math.sin(angle) : Math.cos(angle)) * Math.sqrt(1 / this.ds);
})),
tqry: new Matrix(this.device, Float32Array, mat(this.ds, this.ds, () => (Math.random() - 0.5) * Math.sqrt(1 / this.ds))),
tkey: new Matrix(this.device, Float32Array, mat(this.ds, this.ds, () => (Math.random() - 0.5) * Math.sqrt(1 / this.ds))),
tval: new Matrix(this.device, Float32Array, mat(this.ds, this.ds, () => (Math.random() - 0.5) * Math.sqrt(1 / this.ds))),
tout: new Matrix(this.device, Float32Array, mat(this.vs, this.ds, () => (Math.random() - 0.5) * Math.sqrt(1 / this.ds))),
};
} else {
this.weights = {
inp: new Matrix(this.device, Float32Array, data.weights.inp),
pos: new Matrix(this.device, Float32Array, data.weights.pos),
tqry: new Matrix(this.device, Float32Array, data.weights.tqry),
tkey: new Matrix(this.device, Float32Array, data.weights.tkey),
tval: new Matrix(this.device, Float32Array, data.weights.tval),
tout: new Matrix(this.device, Float32Array, data.weights.out),
};
this.stage = data.stage;
this.samples = data.samples;
if (data.tokenizer !== `${tokenizer.tokens.length}${tokenizer.tokens[0]}`) {
throw new Error(`The model was trained against a different tokenizer ("${data.tokenizer}").`);
}
}
this.processing = {
ds: new Scalar<Float32Array>(this.device, Float32Array, 1 / Math.sqrt(this.ds)),
lr: new Scalar<Float32Array>(this.device, Float32Array, 0),
itids: new Vector<W, Int32Array, TID>(this.device, Int32Array, this.ws),
ilogits: new Matrix<W, V, Float32Array>(this.device, Float32Array, this.ws, this.vs),
e: new Matrix<W, D, Float32Array>(this.device, Float32Array, this.ws, this.ds),
q: new Matrix<W, D, Float32Array>(this.device, Float32Array, this.ws, this.ds),
k: new Matrix<W, D, Float32Array>(this.device, Float32Array, this.ws, this.ds),
tk: new Matrix<D, W, Float32Array>(this.device, Float32Array, this.ds, this.ws),
qtk: new Matrix<W, W, Float32Array>(this.device, Float32Array, this.ws, this.ws),
v: new Matrix<W, D, Float32Array>(this.device, Float32Array, this.ws, this.ds),
s: new Matrix<W, W, Float32Array>(this.device, Float32Array, this.ws, this.ws),
se: new Matrix<W, W, Float32Array>(this.device, Float32Array, this.ws, this.ws),
wa: new Matrix<W, W, Float32Array>(this.device, Float32Array, this.ws, this.ws),
a: new Matrix<W, D, Float32Array>(this.device, Float32Array, this.ws, this.ds),
o: new Matrix<W, V, Float32Array>(this.device, Float32Array, this.ws, this.vs),
temp: new Scalar<Float32Array>(this.device, Float32Array, 1),
tempo: new Matrix<W, V, Float32Array>(this.device, Float32Array, this.ws, this.vs),
on: new Matrix<W, V, Float32Array>(this.device, Float32Array, this.ws, this.vs),
ologits: new Matrix<W, V, Float32Array>(this.device, Float32Array, this.ws, this.vs),
dldo: new Matrix<W, V, Float32Array>(this.device, Float32Array, this.ws, this.vs),
otdldo: new Matrix<D, W, Float32Array>(this.device, Float32Array, this.ds, this.ws),
totdldowa: new Matrix<W, D, Float32Array>(this.device, Float32Array, this.ws, this.ds),
dlds: new Matrix<W, W, Float32Array>(this.device, Float32Array, this.ws, this.ws),
dldsk: new Matrix<W, D, Float32Array>(this.device, Float32Array, this.ws, this.ds),
dldsq: new Matrix<W, D, Float32Array>(this.device, Float32Array, this.ws, this.ds),
ginp: new Matrix(this.device, Float32Array, this.vs, this.ds),
gtqry: new Matrix(this.device, Float32Array, this.ds, this.ds),
gtkey: new Matrix(this.device, Float32Array, this.ds, this.ds),
gtval: new Matrix(this.device, Float32Array, this.ds, this.ds),
gtout: new Matrix(this.device, Float32Array, this.vs, this.ds),
rnd: new Vector<W, Float32Array>(this.device, Float32Array, this.ws),
otids: new Vector<W, Int32Array, TID>(this.device, Int32Array, this.ws),
ttids: new Vector<W, Int32Array, TID>(this.device, Int32Array, this.ws),
};
}
public async debug(
sample: number,
itids: Vector<W, Int32Array, TID> = this.processing.itids,
ttids: Vector<W, Int32Array, TID> = this.processing.ttids,
o: Matrix<W, V, Float32Array> = this.processing.o,
ologits: Matrix<W, V, Float32Array> = this.processing.ologits,
file?: Writable,
llo: boolean = false,
): Promise<void> {
if (!file) {
return;
}
for (let token = llo ? itids.dims - 1 : 0; token < itids.dims; token++) {
const os = (await o.get())[token];
const ls = (await ologits.get())[token]
.map((p, tid) => [p, tid])
.sort(([l], [r]) => r - l);
const point: LogitsPoint = {
sample,
token,
itid: this.tokenizer.detokenize([await itids.get(token)])[0],
ttid: this.tokenizer.detokenize([await ttids.get(token)])[0],
t0: this.tokenizer.detokenize([ls[0][1]])[0],
o0: os[ls[0][1]],
p0: ls[0][0],
t1: this.tokenizer.detokenize([ls[1][1]])[0],
o1: os[ls[1][1]],
p1: ls[1][0],
t2: this.tokenizer.detokenize([ls[2][1]])[0],
o2: os[ls[2][1]],
p2: ls[2][0],
tNm2: this.tokenizer.detokenize([ls[ls.length - 3][1]])[0],
oNm2: os[ls[ls.length - 3][1]],
pNm2: ls[ls.length - 3][0],
tNm1: this.tokenizer.detokenize([ls[ls.length - 2][1]])[0],
oNm1: os[ls[ls.length - 3][1]],
pNm1: ls[ls.length - 2][0],
tN: this.tokenizer.detokenize([ls[ls.length - 1][1]])[0],
oN: os[ls[ls.length - 1][1]],
pN: ls[ls.length - 1][0],
sum: (await ologits.get())[token].reduce((a, b) => a + b, 0),
stddev: stddev((await o.get())[token]),
entropy: -(await ologits.get())[token].reduce((sum, p) => sum + (p > 0 ? p * Math.log(p) : 0), 0),
loss: -Math.log((await ologits.get())[token][await ttids.get(token)] + 1e-10),
};
file.write(`${JSON.stringify(Object.values(point)).slice(1, -1)}\n`);
}
}
public async train(
duration: number | 'auto',
data: string,
{ logs = process.stderr, stats, logits, samples, snapshot, statsInterval = 5000, snapshotInterval = 5000 }: {
logs?: Writable;
stats?: Writable;
logits?: Writable;
samples?: Writable;
snapshot?: (json: object) => void;
statsInterval?: number;
snapshotInterval?: number;
} = {},
): Promise<void> {
logs.write('Training model...\n');
let logged = Date.now();
let snapshotted = Date.now();
const aw: number = 10;
const times = [];
const losses = [];
let epochs = duration === 'auto' ? Infinity : +duration;
const tids = this.tokenizer.tokenize(data);
const batches = tids.length - this.ws - 1;
let lr = 0.003;
for (let epoch = 0; epoch < epochs; epoch++) {
const idxs = randseq(tids.length);
for (let batch = 0; batch < batches; batch++) {
const begin: number = Date.now();
const si = idxs[batch];
await this.processing.itids.set(tids.slice(si, si + this.ws) as CPUVector<W>);
await this.processing.ttids.set(tids.slice(si + 1, si + 1 + this.ws) as CPUVector<W>);
const ilogits = await this.ilogits(this.processing.itids);
const e = await this.e(ilogits);
const q = this.q(e);
const k = this.k(e);
const v = this.v(e);
const s = this.s(q, k);
const wa = await this.wa(s);
const a = this.a(wa, v);
const o = this.o(a);
const ologits = await this.ologits(o);
await this.upd(ilogits, e, q, k, v, wa, a, ologits, this.processing.ttids, lr);
const queued: number = Date.now();
await this.device.queue.onSubmittedWorkDone();
const end: number = Date.now();
this.samples += 1;
times.push(end - begin);
if (
epoch === 0 && batch === 0
|| batch === batches - 1
|| end - logged > statsInterval
) {
const point: TrainingPoint = {
index: this.samples,
timestamp: Date.now(),
epoch,
batch,
lr,
loss: 0,
accuracy: 0,
ginp: (await this.processing.ginp.get()).reduce((s, r) => r.reduce((s, v) => s + v * v, s), 0) / lr / lr,
gqry: (await this.processing.gtqry.get()).reduce((s, r) => r.reduce((s, v) => s + v * v, s), 0) / lr / lr,
gkey: (await this.processing.gtkey.get()).reduce((s, r) => r.reduce((s, v) => s + v * v, s), 0) / lr / lr,
gval: (await this.processing.gtval.get()).reduce((s, r) => r.reduce((s, v) => s + v * v, s), 0) / lr / lr,
gout: (await this.processing.gtout.get()).reduce((s, r) => r.reduce((s, v) => s + v * v, s), 0) / lr / lr,
queueing: 0,
computing: 0,
inferring: 0,
};
point.queueing = Date.now() - begin;
point.computing = Date.now() - queued;
const out = await this.otids(await this.rnd(), ologits).get();
const tgt = await this.processing.ttids.get();
const inferred: number = Date.now();
point.inferring = inferred - end;
point.accuracy = tgt.filter((t, i) => out[i] === t).length / tgt.length;
point.loss = this.loss(await ologits.get(), tgt);
losses.push(point.loss);
if (this.stage === 'early') {
lr = Math.max(0.001, Math.min(0.003, point.loss * 0.001));
if (losses.length > aw && (slope(losses.slice(-aw)) > -0.005 || stddev(losses.slice(-aw)) < 0.2)) {
logs?.write('Switching to mid stage.\n');
this.stage = 'mid';
}
} else if (this.stage === 'mid') {
lr = Math.max(0.0003, lr * 0.98);
if (losses.length > aw && Math.abs(slope(losses.slice(-aw))) < 0.002 && stddev(losses.slice(-aw)) < 0.05) {
logs?.write('Switching to end stage.\n');
this.stage = 'end';
}
} else {
lr = Math.max(1e-4, lr * 0.98);
}
logs?.write(`Done ${epoch * batches + batch + 1} / ${epochs * batches}, ETA ${Math.round(drm(times) * (epochs * batches - times.length) / 1000)}s\n`);
stats?.write(`${Object.values(point).join(',')}\n`);
await this.debug(this.samples, this.processing.itids, this.processing.ttids, o, ologits, logits);
logged = end;
}
if (batch === 0 && end - snapshotted > snapshotInterval) {
if (duration === 'auto' && this.stage === 'end' && losses.length > aw && (
Math.abs(slope(losses.slice(-aw))) < 0.001 && stddev(losses.slice(-aw)) < 0.001
|| slope(losses.slice(-aw)) > 0.001
|| stddev(losses.slice(-aw)) > 1)) {
logs?.write(`Ending training with slope=${slope(losses.slice(-aw))} stddev=${stddev(losses.slice(-aw))}.\n`);
epochs = 0;
break;
}
snapshot?.(await this.toJSON());
samples?.write(`Epoch ${epoch + 1}:\n`);
samples?.write('Sample (temp=0.3):\n');
samples?.write(this.tokenizer.detokenize(await this.run(this.tokenizer.tokenize('Hello?'), { temp: 0.3 })).join(''));
samples?.write('\nEnd of sample.\n');
samples?.write('Sample (temp=0.7):\n');
samples?.write(this.tokenizer.detokenize(await this.run(this.tokenizer.tokenize('Hello?'), { temp: 0.7 })).join(''));
samples?.write('\nEnd of sample.\n');
samples?.write('Sample (temp=1):\n');
samples?.write(this.tokenizer.detokenize(await this.run(this.tokenizer.tokenize('Hello?'), { temp: 1 })).join(''));
samples?.write('\nEnd of sample.\n');
samples?.write('Sample (temp=1.3):\n');
samples?.write(this.tokenizer.detokenize(await this.run(this.tokenizer.tokenize('Hello?'), { temp: 1.3 })).join(''));
samples?.write('\nEnd of sample.\n\n');
samples?.write('Sample (argmax):\n');
samples?.write(this.tokenizer.detokenize(await this.run(this.tokenizer.tokenize('Hello?'), { temp: 'max' })).join(''));
samples?.write('\nEnd of sample.\n\n');
snapshotted = end;
}
}
}
logs?.write('Training completed.\n');
}
public async transform(
tids: TID[],
{ temp = 1, logits, llo = false }: {
temp?: number | 'max';
logits?: Writable;
llo?: boolean;
} = {},
): Promise<[TID[], TID[]]> {
const itids = [
...Array(Math.max(0, this.ws - tids.length)).fill(0),
...tids.slice(-this.ws),
];
await this.processing.itids.set(itids);
await this.processing.ttids.set([...itids.slice(1), 0]);
const ilogits = await this.ilogits(this.processing.itids);
const e = await this.e(ilogits);
const q = this.q(e);
const k = this.k(e);
const v = this.v(e);
const s = this.s(q, k);
const wa = await this.wa(s);
const a = this.a(wa, v);
const o = this.o(a);
const ologits = await this.ologits(o, temp === 'max' ? 1 : temp);
const out = temp === 'max' ? this.otids(ologits) : this.otids(await this.rnd(), ologits);
const otids = await out.get();
await this.debug(0, this.processing.itids, this.processing.ttids, o, ologits, logits, llo);
return [itids, otids];
}
public async run(
tids: TID[],
{ temp = 1, samples = this.ws, logits }: {
temp?: number | 'max';
samples?: number;
logits?: Writable;
} = {},
): Promise<TID[]> {
const result: TID[] = [];
while (result.length < samples) {
const [_, out] = await this.transform(tids, { temp, logits, llo: true });
const next: TID = out[this.ws - 1];
result.push(next);
tids.shift();
tids.push(next);
}
return result;
}
public async toJSON(): Promise<ModelData<V, W, D>> {
return {
tokenizer: `${this.tokenizer.tokens.length}${this.tokenizer.tokens[0]}`,
vs: this.vs,
ws: this.ws,
ds: this.ds,
stage: this.stage,
samples: this.samples,
weights: {
inp: await this.weights.inp.get(),
pos: await this.weights.pos.get(),
tqry: await this.weights.tqry.get(),
tkey: await this.weights.tkey.get(),
tval: await this.weights.tval.get(),
out: await this.weights.tout.get(),
},
};
}
}

@ -0,0 +1,160 @@
import { GroupQueueBase, QueueItem } from './data/group-queue';
import { List, ListItem } from './data/list';
import { Trie } from './data/trie';
interface InternalStringPairQueueItem extends QueueItem<ListItem<string>> {
prev?: StringPairQueueItem;
next?: StringPairQueueItem;
}
interface StringPairQueueItem extends QueueItem<ListItem<string>> {
readonly prev?: StringPairQueueItem;
readonly next?: StringPairQueueItem;
}
class StringPairQueue extends GroupQueueBase<string, ListItem<string>> {
protected items: Map<ListItem<string>, StringPairQueueItem>;
public constructor(values?: Iterable<ListItem<string>>) {
super();
this.items = new Map();
for (const v of values ?? []) {
this.insert(v);
}
}
protected override hash(v: ListItem<string>): string {
return `${v.value}$$$$${v.next.value}`;
}
override insert(value: ListItem<string>): StringPairQueueItem {
const item: StringPairQueueItem = super.insert(value);
const prev: StringPairQueueItem | undefined = this.items.get(value.prev);
const next: StringPairQueueItem | undefined = this.items.get(value.next);
(item as InternalStringPairQueueItem).prev = prev;
(item as InternalStringPairQueueItem).next = next;
if (prev) {
(prev as InternalStringPairQueueItem).next = item;
}
if (next) {
(next as InternalStringPairQueueItem).prev = item;
}
this.items.set(value, item);
return item;
}
override remove(item: StringPairQueueItem): boolean {
if (!super.remove(item)) {
return false;
}
if (item.prev) {
(item.prev as InternalStringPairQueueItem).next = undefined;
}
if (item.next) {
(item.next as InternalStringPairQueueItem).prev = undefined;
}
this.items.delete(item.value);
return true;
}
}
export type TID = number;
export class Tokenizer {
protected trie: Trie;
protected list: string[];
public get tokens(): Readonly<string[]> {
return this.list;
}
protected train(size: number, text: string): void {
if (!text) {
return;
}
console.log('Gathering pairs.');
let log: number = Date.now();
const samples: number[] = [];
const tokens = new List(text);
const pairs: StringPairQueue = new StringPairQueue(tokens.items());
const vocab = new Set<string>(tokens);
console.log('Merging pairs.');
while (vocab.size < size) {
const begin: number = Date.now();
const items: List<StringPairQueueItem> | undefined = pairs.peek();
if (!items?.length) {
console.log('No merge found.');
break;
} else {
const { value: { value: a, next: { value: b } } } = items.first!;
const merged: string = a + b;
for (const item of items) {
if (!item.orphan) {
if (item.prev) {
pairs.remove(item.prev);
}
if (item.next) {
pairs.remove(item.next);
}
pairs.remove(item);
tokens.set(item.value, merged);
tokens.delete(item.value.next);
pairs.insert(item.value.prev);
pairs.insert(item.value);
}
}
vocab.add(merged);
const end: number = Date.now();
samples.push(end - begin);
if (end - log > 5000) {
console.log(`Merged ${vocab.size} / ${size}, ETA ${Math.round(samples.reduce((a, b) => a + b, 0) / samples.length * (size - vocab.size) / 1000)}s`);
log = end;
}
}
}
this.trie = new Trie([...vocab].sort((l, r) => r.length - l.length));
}
public constructor(size: number, text: string);
public constructor(data: object);
public constructor(sizeOrData: number | object, text?: string) {
if (text) {
this.trie = new Trie();
this.train(sizeOrData as number, text!);
} else {
this.trie = Trie.fromJSON(sizeOrData as object);
}
this.list = [...this.trie];
}
public tokenize(text: string): TID[] {
const tokens: string[] = [];
while (text.length) {
const match = this.trie.match(text);
tokens.push(match ?? text[0]);
text = text.slice(match?.length ?? 1);
}
return tokens.map(t => this.list.indexOf(t));
}
public detokenize(tids: TID[]): string[] {
return tids.map(t => this.list[t]);
}
public toJSON(): object {
return this.trie.toJSON();
}
}

307923
sys

File diff suppressed because it is too large Load Diff

@ -0,0 +1,57 @@
import { describe, expect, it } from '@jest/globals';
import { GroupQueue, QueueItem } from '../src/data/group-queue';
import { List } from '../src/data/list';
describe('GroupQueue', () => {
function json<T>(list: List<QueueItem<T>>): T[] {
return [...list].map(({ value }) => value);
}
describe('from array', () => {
it('[]', () => expect([...new GroupQueue([])].map(json)).toEqual([]));
it('[1]', () => expect([...new GroupQueue([1])].map(json)).toEqual([[1]]));
it('["b", "c", "b", "c"]', () => expect([...new GroupQueue(['b', 'c', 'b', 'c'])].map(json)).toEqual([['b', 'b'], ['c', 'c']]));
});
describe('length', () => {
it('[]', () => expect(new GroupQueue([]).length).toEqual(0));
it('[1]', () => expect(new GroupQueue([1]).length).toEqual(1));
it('["b", "c", "b", "c"]', () => expect(new GroupQueue(['b', 'c', 'b', 'c']).length).toEqual(2));
});
describe('insert', () => {
function insert<T>(queue: GroupQueue<T>, value: T): T[][] {
queue.insert(value);
return [...queue].map(json);
}
it('0 -> []', () => expect([...insert(new GroupQueue<number>([]), 0)]).toEqual([[0]]));
it('0 -> [1]', () => expect([...insert(new GroupQueue([1]), 0)]).toEqual([[1], [0]]));
it('1 -> [1]', () => expect([...insert(new GroupQueue([1]), 1)]).toEqual([[1, 1]]));
it('"d" -> ["b", "c", "b", "c"]', () => expect([...insert(new GroupQueue(['b', 'c', 'b', 'c']), 'd')]).toEqual([['b', 'b'], ['c', 'c'], ['d']]));
it('"b" -> ["b", "c", "b", "c"]', () => expect([...insert(new GroupQueue(['b', 'c', 'b', 'c']), 'b')]).toEqual([['b', 'b', 'b'], ['c', 'c']]));
});
describe('itemOf / remove', () => {
function remove<T>(queue: GroupQueue<T>, value: T): T[][] {
const item = queue.itemOf(value);
if (item) {
queue.remove(item);
}
return [...queue].map(json);
}
it('[] - 0', () => expect([...remove(new GroupQueue<number>([]), 0)]).toEqual([]));
it('[1] - 0', () => expect([...remove(new GroupQueue([1]), 0)]).toEqual([[1]]));
it('[1] - 1', () => expect([...remove(new GroupQueue([1]), 1)]).toEqual([]));
it('["b", "c", "b", "c"] - "d"', () => expect([...remove(new GroupQueue(['b', 'c', 'b', 'c']), 'd')]).toEqual([['b', 'b'], ['c', 'c']]));
it('["b", "c", "b", "c"] - "b"', () => expect([...remove(new GroupQueue(['b', 'c', 'b', 'c']), 'b')]).toEqual([['c', 'c'], ['b']]));
});
describe('pop', () => {
function pop<T>(queue: GroupQueue<T>): [T[], T[][]] {
return [[...queue.pop() ?? []].map(({ value }) => value), [...queue].map(json)];
}
it('[]', () => expect([...pop(new GroupQueue<number>([]))]).toEqual([[], []]));
it('[1]', () => expect([...pop(new GroupQueue([1]))]).toEqual([[1], []]));
it('["b", "c", "b", "c"]', () => expect([...pop(new GroupQueue(['b', 'c', 'b', 'c']))]).toEqual([['b', 'b'], [['c', 'c']]]));
});
});

@ -0,0 +1,88 @@
import { describe, expect, it } from '@jest/globals';
import { List, ListItem } from '../src/data/list';
describe('List', () => {
describe('from / to array', () => {
it('[]', () => expect([...new List([])]).toEqual([]));
it('[1]', () => expect([...new List([1])]).toEqual([1]));
it('["b", "c", "b", "c"]', () => expect([...new List(['b', 'c', 'b', 'c'])]).toEqual(['b', 'c', 'b', 'c']));
});
describe('length', () => {
it('[]', () => expect(new List([]).length).toEqual(0));
it('[1]', () => expect(new List([1]).length).toEqual(1));
it('["b", "c", "b", "c"]', () => expect(new List(['b', 'c', 'b', 'c']).length).toEqual(4));
});
describe('first', () => {
it('[]', () => expect(new List([]).first).toBeUndefined());
it('[1]', () => expect(new List([1]).first).toEqual(1));
it('["b", "c", "b", "c"]', () => expect(new List(['b', 'c', 'b', 'c']).first).toEqual('b'));
});
describe('last', () => {
it('[]', () => expect(new List([]).last).toBeUndefined());
it('[1]', () => expect(new List([1]).last).toEqual(1));
it('["b", "c", "b", "c"]', () => expect(new List(['b', 'c', 'b', 'c']).last).toEqual('c'));
});
describe('prepend', () => {
function prepend<T>(list: List<T>, value: T): List<T> {
list.prepend(value);
return list;
}
it('0 -> []', () => expect([...prepend(new List<number>([]), 0)]).toEqual([0]));
it('0 -> [1]', () => expect([...prepend(new List([1]), 0)]).toEqual([0, 1]));
it('"d" -> ["b", "c", "b", "c"]', () => expect([...prepend(new List(['b', 'c', 'b', 'c']), 'd')]).toEqual(['d', 'b', 'c', 'b', 'c']));
});
describe('append', () => {
function append<T>(list: List<T>, value: T): List<T> {
list.append(value);
return list;
}
it('[] <- 0', () => expect([...append(new List<number>([]), 0)]).toEqual([0]));
it('[1] <- 0', () => expect([...append(new List([1]), 0)]).toEqual([1, 0]));
it('["b", "c", "b", "c"] <- "d"', () => expect([...append(new List(['b', 'c', 'b', 'c']), 'd')]).toEqual(['b', 'c', 'b', 'c', 'd']));
});
describe('includes', () => {
it('[] not includes 0', () => expect(new List<number>([]).includes(0)).toBe(false));
it('[1] not includes 0', () => expect(new List([1]).includes(0)).toBe(false));
it('[1] includes 1', () => expect(new List([1]).includes(1)).toBe(true));
it('["b", "c", "b", "c"] not includes "d"', () => expect(new List(['b', 'c', 'b', 'c']).includes('d')).toBe(false));
it('["b", "c", "b", "c"] includes "b"', () => expect(new List(['b', 'c', 'b', 'c']).includes('b')).toBe(true));
});
describe('itemOf / set', () => {
function update<T>(list: List<T>, from: T, to: T): List<T> {
const item: ListItem<T> | undefined = list.itemOf(from);
if (item) {
list.set(item, to);
}
return list;
}
it('[]: 0 -> 1', () => expect([...update(new List<number>([]), 0, 1)]).toEqual([]));
it('[1]: 0 -> 1', () => expect([...update(new List([1]), 0, 1)]).toEqual([1]));
it('[1]: 1 -> 0', () => expect([...update(new List([1]), 1, 0)]).toEqual([0]));
it('["b", "c", "b", "c"]: "b" -> "d"', () => expect([...update(new List(['b', 'c', 'b', 'c']), 'b', 'd')]).toEqual(['d', 'c', 'b', 'c']));
it('["b", "c", "b", "c"]: "d" -> "b"', () => expect([...update(new List(['b', 'c', 'b', 'c']), 'd', 'b')]).toEqual(['b', 'c', 'b', 'c']));
});
describe('itemOf / delete', () => {
function remove<T>(list: List<T>, value: T): List<T> {
const item: ListItem<T> | undefined = list.itemOf(value);
if (item) {
list.delete(item);
}
return list;
}
it('[] - 0', () => expect([...remove(new List<number>([]), 0)]).toEqual([]));
it('[1] - 0', () => expect([...remove(new List([1]), 0)]).toEqual([1]));
it('[1] - 1', () => expect([...remove(new List([1]), 1)]).toEqual([]));
it('["b", "c", "b", "c"] - "b"', () => expect([...remove(new List(['b', 'c', 'b', 'c']), 'b')]).toEqual(['c', 'b', 'c']));
it('["b", "c", "b", "c"] - "d"', () => expect([...remove(new List(['b', 'c', 'b', 'c']), 'd')]).toEqual(['b', 'c', 'b', 'c']));
});
});

@ -0,0 +1,7 @@
{
"extends": "../tsconfig.json",
"include": [
"../src/**/*.ts",
"**/*.ts",
],
}

@ -0,0 +1,35 @@
#!/bin/bash
model=$1
count=${2:-9000}
mkdir -p ${model}/downloads
rm -rf ${model}/downloads/*
BOOK_IDS=(
174 1342 11 2701 84 2600 1661 215 1952 98 345 35 1497 2147 4300
)
echo "Downloading books..."
i=1
for ID in "${BOOK_IDS[@]}"; do
echo "Fetching $ID..."
curl -s -L "https://www.gutenberg.org/files/$ID/$ID-0.txt" -o "${model}/downloads/$ID.txt"
i=$((i+1))
if [ ${i} -gt ${count} ]; then
break;
fi
done
echo "Concatenating into corpus.txt..."
# Remove headers/footers and join them
rm -f ${model}/corpus.txt
for FILE in ${model}/downloads/*.txt; do
awk '/\*\*\* START OF/,/\*\*\* END OF/ { if (!/START|END/) print }' "$FILE" >> ${model}/corpus.txt
echo >> ${model}/corpus.txt
done
echo "All done. Corpus size:"
du -h ${model}/corpus.txt

@ -0,0 +1,178 @@
import sys
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import matplotlib.dates as mdates # Add this import at the top
import select
from datetime import datetime
import threading
import queue
fig = plt.figure(figsize=(10, 6))
ax_lr = fig.add_axes([0.05, 0.3, 0.9, 0.65])
ax_lr.set_facecolor("none")
ax_lr.set_ylim(bottom=0, top=None)
ax_lr.yaxis.set_ticks_position('right')
ax_lr.tick_params(axis='y', labelcolor='tab:brown')
ax_acc = fig.add_axes([0.05, 0.3, 0.9, 0.65], sharex=ax_lr)
ax_acc.set_facecolor("none")
ax_acc.set_ylim(0, 100)
ax_acc.yaxis.set_ticks_position('right')
ax_acc.tick_params(axis='y', labelcolor='tab:green')
ax_loss = fig.add_axes([0.05, 0.3, 0.9, 0.65], sharex=ax_lr)
ax_loss.set_facecolor("none")
ax_loss.set_ylim(bottom=0, top=None)
ax_loss.tick_params(axis='y', labelcolor='tab:blue')
ax_time = fig.add_axes([0.05, 0.05, 0.9, 0.2])
ax_time.set_facecolor("none")
ax_time.set_ylim(bottom=0, top=None)
ax_time.yaxis.set_ticks_position('right')
ax_time.tick_params(axis='y', labelcolor='tab:orange')
columns = ["index", "timestamp", "epoch", "batch", "lr", "loss", "accuracy", "grad_inp", "grad_qry", "grad_key", "grad_val", "grad_out", "queue_ms", "compute_ms", "infer_ms"]
df = pd.DataFrame(columns=columns)
ema_alpha = 0.1
window_size = 50
def parse_line(line):
try:
parts = line.strip().split(",")
if parts[0] == "index" or len(parts) < len(columns):
return None
row = {
"index": int(parts[0]),
"timestamp": datetime.fromtimestamp(int(parts[1]) / 1000),
"epoch": int(parts[2]),
"batch": int(parts[3]),
"lr": float(parts[4]),
"loss": float(parts[5]),
"accuracy": float(parts[6]) * 100,
"grad_inp": float(parts[7]),
"grad_qry": float(parts[8]),
"grad_key": float(parts[9]),
"grad_val": float(parts[10]),
"grad_out": float(parts[11]),
"queue_ms": float(parts[12]),
"compute_ms": float(parts[13]),
"infer_ms": float(parts[14]),
}
return row
except Exception as e:
print(f"Bad line: {line.strip()} ({e})", file=sys.stderr)
return None
line_queue = queue.Queue()
def stdin_reader():
for line in sys.stdin:
line_queue.put(line)
threading.Thread(target=stdin_reader, daemon=True).start()
def get_new_lines():
lines = []
while not line_queue.empty():
lines.append(line_queue.get())
return lines
last_seen_index = None
def animate(_):
global df
lines = get_new_lines()
if not lines:
return
new_rows = [parse_line(line) for line in lines]
new_rows = [r for r in new_rows if r is not None]
if not new_rows:
return
global last_seen_index
new_index = new_rows[-1]["index"]
if last_seen_index and (new_index < last_seen_index):
print("Index reset, restarting plot")
df = pd.DataFrame(columns=columns)
last_seen_index = new_index
df_new = pd.DataFrame(new_rows)
df_new["total_ms"] = df_new["queue_ms"] + df_new["compute_ms"] + df_new["infer_ms"]
df = pd.concat([df, df_new], ignore_index=True)
df["ema_loss"] = df["loss"].ewm(alpha=ema_alpha).mean().combine_first(df["loss"])
df["avg_loss"] = df["loss"].rolling(window=window_size).mean().combine_first(df["loss"])
df["avg_accuracy"] = df["accuracy"].rolling(window=window_size).mean().combine_first(df["accuracy"])
df["avg_total_ms"] = df["total_ms"].rolling(window=window_size).mean().combine_first(df["total_ms"])
ax_lr.clear()
ax_lr.set_ylabel("LR")
ax_lr.yaxis.set_label_position("right")
ax_acc.clear()
ax_acc.set_ylabel("Accuracy (%)")
ax_loss.clear()
ax_loss.set_title("Live Training Metrics")
ax_loss.set_xlabel("Epoch / Batch")
ax_loss.set_ylabel("Loss")
ax_acc.yaxis.set_label_position("right")
ax_time.clear()
ax_time.set_xlabel("Time")
ax_time.set_ylabel("Processing Time (ms)")
ax_time.yaxis.set_label_position("right")
ax_lr.plot(df["index"], df["lr"], label="LR", linestyle='--', alpha=0.3, color="tab:brown")
ax_acc.plot(df["index"], df["accuracy"], label="Accuracy", linestyle='--', alpha=0.3, color="tab:green")
ax_acc.plot(df["index"], df["avg_accuracy"], label="Avg Accuracy", color="tab:green")
ax_loss.plot(df["index"], df["loss"], label="Loss", linestyle=':', alpha=0.3, color="tab:blue")
ax_loss.plot(df["index"], df["ema_loss"], label="EMA Loss", linestyle='--', alpha=0.7, color="tab:blue")
ax_loss.plot(df["index"], df["avg_loss"], label=f"Avg Loss", color="tab:blue")
ax_time.plot(df["index"], df["queue_ms"], label="Queueing", linestyle=':', alpha=0.3, color="#ffa64d")
ax_time.plot(df["index"], df["compute_ms"], label="Computing", linestyle=':', alpha=0.3, color="#cc6600")
ax_time.plot(df["index"], df["infer_ms"], label="Inferring", linestyle=':', alpha=0.3, color="#ffb84d")
ax_time.plot(df["index"], df["total_ms"], label="Total Time",linestyle='--', alpha=0.7, color="tab:orange")
ax_time.plot(df["index"], df["avg_total_ms"], label="Avg Total Time", color="tab:orange")
# Add epoch/batch label ticks to secondary X axis
tick_locs = ax_time.get_xticks()
tick_labels = []
if not df.empty:
for loc in tick_locs:
idx = (abs(df["index"] - loc)).argmin()
e, b = int(df.iloc[idx]["epoch"]), int(df.iloc[idx]["batch"])
tick_labels.append(f"{e + 1}/{b + 1}")
else:
tick_labels = [""] * len(tick_locs)
ax_loss.set_xlim(ax_time.get_xlim())
ax_loss.set_xticks(tick_locs)
ax_loss.set_xticklabels(tick_labels)
# Gather all legend entries
lines = []
labels = []
for ax in [ax_lr, ax_acc, ax_loss]:
l, lab = ax.get_legend_handles_labels()
lines += l
labels += lab
# Add combined legend to ax_loss
ax_loss.legend(lines, labels, loc='upper left') # or 'upper right', etc.
ax_time.legend(loc='upper left')
ani = animation.FuncAnimation(fig, animate, interval=1000)
plt.show()

@ -0,0 +1,24 @@
{
"compilerOptions": {
"baseUrl": ".",
"outDir": "build",
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "Node",
"esModuleInterop": true,
"lib": [
"DOM", "ES2022"
],
"noImplicitAny": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"strictNullChecks": true,
"strictPropertyInitialization": true,
"declaration": false,
"sourceMap": true,
},
"include": [
"src/**/*.ts",
],
}

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save