@driftengine/texture 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +29 -0
- package/README.md +106 -0
- package/dist/decodeCpu.d.ts +59 -0
- package/dist/decodeCpu.js +234 -0
- package/dist/decodeGraph.d.ts +105 -0
- package/dist/decodeGraph.js +180 -0
- package/dist/half.d.ts +24 -0
- package/dist/half.js +86 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.js +55 -0
- package/dist/inference.d.ts +53 -0
- package/dist/inference.js +243 -0
- package/dist/materialArray.d.ts +38 -0
- package/dist/materialArray.js +40 -0
- package/dist/mipNdf.d.ts +29 -0
- package/dist/mipNdf.js +53 -0
- package/dist/overlay/journal.d.ts +78 -0
- package/dist/overlay/journal.js +171 -0
- package/dist/overlay/sparse.d.ts +68 -0
- package/dist/overlay/sparse.js +212 -0
- package/dist/progressive.d.ts +30 -0
- package/dist/progressive.js +56 -0
- package/dist/residency/pageCache.d.ts +103 -0
- package/dist/residency/pageCache.js +184 -0
- package/dist/residency/predict.d.ts +55 -0
- package/dist/residency/predict.js +51 -0
- package/dist/residency/predictor.d.ts +16 -0
- package/dist/residency/predictor.js +44 -0
- package/dist/residency/queue.d.ts +26 -0
- package/dist/residency/queue.js +52 -0
- package/dist/residency/stream.d.ts +66 -0
- package/dist/residency/stream.js +142 -0
- package/dist/residency/table.d.ts +36 -0
- package/dist/residency/table.js +72 -0
- package/dist/residency/viewTiles.d.ts +108 -0
- package/dist/residency/viewTiles.js +419 -0
- package/dist/semantics.d.ts +52 -0
- package/dist/semantics.js +76 -0
- package/dist/tensor/architecture.d.ts +53 -0
- package/dist/tensor/architecture.js +96 -0
- package/dist/tensor/attention.d.ts +5 -0
- package/dist/tensor/attention.js +62 -0
- package/dist/tensor/denseOperators.d.ts +2 -0
- package/dist/tensor/denseOperators.js +136 -0
- package/dist/tensor/graph.d.ts +83 -0
- package/dist/tensor/graph.js +175 -0
- package/dist/tensor/linear.d.ts +49 -0
- package/dist/tensor/linear.js +136 -0
- package/dist/tensor/operatorKit.d.ts +27 -0
- package/dist/tensor/operatorKit.js +45 -0
- package/dist/tensor/operators.d.ts +3 -0
- package/dist/tensor/operators.js +24 -0
- package/dist/tensor/resize.d.ts +6 -0
- package/dist/tensor/resize.js +107 -0
- package/dist/tensor/reuse.d.ts +33 -0
- package/dist/tensor/reuse.js +59 -0
- package/dist/tensor/shapeOperators.d.ts +3 -0
- package/dist/tensor/shapeOperators.js +173 -0
- package/dist/tensor/spatial.d.ts +34 -0
- package/dist/tensor/spatial.js +131 -0
- package/dist/tensor/spatialOperators.d.ts +2 -0
- package/dist/tensor/spatialOperators.js +138 -0
- package/dist/tileHash.d.ts +29 -0
- package/dist/tileHash.js +50 -0
- package/dist/timeNodes.d.ts +26 -0
- package/dist/timeNodes.js +48 -0
- package/package.json +59 -0
- package/src/decodeCpu.ts +308 -0
- package/src/decodeGraph.ts +214 -0
- package/src/half.ts +86 -0
- package/src/index.ts +175 -0
- package/src/inference.ts +278 -0
- package/src/materialArray.ts +67 -0
- package/src/mipNdf.ts +63 -0
- package/src/overlay/journal.ts +218 -0
- package/src/overlay/sparse.ts +275 -0
- package/src/progressive.ts +60 -0
- package/src/residency/pageCache.ts +233 -0
- package/src/residency/predict.ts +74 -0
- package/src/residency/predictor.ts +62 -0
- package/src/residency/queue.ts +78 -0
- package/src/residency/stream.ts +194 -0
- package/src/residency/table.ts +89 -0
- package/src/residency/viewTiles.ts +553 -0
- package/src/semantics.ts +114 -0
- package/src/tensor/architecture.ts +140 -0
- package/src/tensor/attention.ts +75 -0
- package/src/tensor/denseOperators.ts +153 -0
- package/src/tensor/graph.ts +244 -0
- package/src/tensor/linear.ts +153 -0
- package/src/tensor/operatorKit.ts +76 -0
- package/src/tensor/operators.ts +28 -0
- package/src/tensor/resize.ts +140 -0
- package/src/tensor/shapeOperators.ts +173 -0
- package/src/tensor/spatial.ts +178 -0
- package/src/tensor/spatialOperators.ts +182 -0
- package/src/tileHash.ts +60 -0
- package/src/timeNodes.ts +60 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A network's definition, written as a function of its weights, built into a graph the runtime runs.
|
|
3
|
+
*
|
|
4
|
+
* **One definition, run over two sources.** At conversion it reads a checkpoint; afterwards it reads
|
|
5
|
+
* the converted file, whenever the runtime needs the graph at a new size — a vision transformer's
|
|
6
|
+
* patch grid follows the image's aspect ratio, and every device kernel bakes its shapes, so a model
|
|
7
|
+
* is one graph per size and not one graph. The two runs agree because of three rules:
|
|
8
|
+
*
|
|
9
|
+
* - **A weight is read by name, at the shape the definition expects**, and a name the source lacks
|
|
10
|
+
* or a shape it disagrees with is refused naming both.
|
|
11
|
+
* - **A weight derived from others** — a batch norm folded into its convolution — is computed once,
|
|
12
|
+
* at conversion, and kept under its own name; a source that already holds that name answers with
|
|
13
|
+
* it and the computation never runs, which is why a converted file needs none of the parts.
|
|
14
|
+
* - **A constant** — a table computed from the shapes, such as rotary angles — carries a name
|
|
15
|
+
* beginning `@`, is rebuilt on every run, and is never taken for a weight.
|
|
16
|
+
*
|
|
17
|
+
* **And every weight the source holds is read, derived from, or set aside by name.** A forgotten
|
|
18
|
+
* bias or layer scale is a network that runs and answers wrongly — the one failure nothing later can
|
|
19
|
+
* see — so a weight left over is refused. What that costs is an explicit `ignore` for every head a
|
|
20
|
+
* definition does not run, which is the point.
|
|
21
|
+
*/
|
|
22
|
+
import { type GraphTensor, type GraphValue, type NetworkGraph } from './graph.ts';
|
|
23
|
+
import type { Attributes } from './operators.ts';
|
|
24
|
+
/** Where a definition's weights come from: a checkpoint being converted, or a converted file. */
|
|
25
|
+
export interface WeightSource {
|
|
26
|
+
get(name: string): GraphTensor | undefined;
|
|
27
|
+
names(): Iterable<string>;
|
|
28
|
+
}
|
|
29
|
+
/** What a constant's name begins with: a table of the shapes, never a weight. */
|
|
30
|
+
export declare const CONSTANT_PREFIX = "@";
|
|
31
|
+
export interface Weights {
|
|
32
|
+
/** The weight `name` as a value of the graph; refused if absent or held at another shape. */
|
|
33
|
+
read(name: string, shape?: readonly number[]): string;
|
|
34
|
+
/**
|
|
35
|
+
* A weight computed once from others, kept under `name`. `compute` reads its parts through
|
|
36
|
+
* `values`, which counts each as read; a source already holding `name` answers with it instead.
|
|
37
|
+
*/
|
|
38
|
+
derive(name: string, shape: readonly number[], compute: (values: (part: string, shape?: readonly number[]) => Float32Array) => Float32Array): string;
|
|
39
|
+
/** A table computed from the graph's shapes, rebuilt on every run and never a weight. */
|
|
40
|
+
constant(name: string, shape: readonly number[], data: Float32Array): string;
|
|
41
|
+
/** Weights the definition deliberately does not read: a name, or a prefix ending in a dot. */
|
|
42
|
+
ignore(nameOrPrefix: string): void;
|
|
43
|
+
}
|
|
44
|
+
export interface GraphBuilder {
|
|
45
|
+
/** A node; its output is named `output` when given, and otherwise by the builder. */
|
|
46
|
+
node(op: string, inputs: readonly string[], attributes?: Attributes, output?: string): string;
|
|
47
|
+
}
|
|
48
|
+
/** A definition: it reads its weights, adds its nodes, and says what goes in and comes out. */
|
|
49
|
+
export type Architecture = (weights: Weights, graph: GraphBuilder) => {
|
|
50
|
+
readonly inputs: readonly GraphValue[];
|
|
51
|
+
readonly outputs: readonly string[];
|
|
52
|
+
};
|
|
53
|
+
export declare function graphFromWeights(source: WeightSource, architecture: Architecture): NetworkGraph;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A network's definition, written as a function of its weights, built into a graph the runtime runs.
|
|
3
|
+
*
|
|
4
|
+
* **One definition, run over two sources.** At conversion it reads a checkpoint; afterwards it reads
|
|
5
|
+
* the converted file, whenever the runtime needs the graph at a new size — a vision transformer's
|
|
6
|
+
* patch grid follows the image's aspect ratio, and every device kernel bakes its shapes, so a model
|
|
7
|
+
* is one graph per size and not one graph. The two runs agree because of three rules:
|
|
8
|
+
*
|
|
9
|
+
* - **A weight is read by name, at the shape the definition expects**, and a name the source lacks
|
|
10
|
+
* or a shape it disagrees with is refused naming both.
|
|
11
|
+
* - **A weight derived from others** — a batch norm folded into its convolution — is computed once,
|
|
12
|
+
* at conversion, and kept under its own name; a source that already holds that name answers with
|
|
13
|
+
* it and the computation never runs, which is why a converted file needs none of the parts.
|
|
14
|
+
* - **A constant** — a table computed from the shapes, such as rotary angles — carries a name
|
|
15
|
+
* beginning `@`, is rebuilt on every run, and is never taken for a weight.
|
|
16
|
+
*
|
|
17
|
+
* **And every weight the source holds is read, derived from, or set aside by name.** A forgotten
|
|
18
|
+
* bias or layer scale is a network that runs and answers wrongly — the one failure nothing later can
|
|
19
|
+
* see — so a weight left over is refused. What that costs is an explicit `ignore` for every head a
|
|
20
|
+
* definition does not run, which is the point.
|
|
21
|
+
*/
|
|
22
|
+
import { validateGraph } from './graph.js';
|
|
23
|
+
/** What a constant's name begins with: a table of the shapes, never a weight. */
|
|
24
|
+
export const CONSTANT_PREFIX = '@';
|
|
25
|
+
const shapeText = (shape) => `[${shape.join(', ')}]`;
|
|
26
|
+
export function graphFromWeights(source, architecture) {
|
|
27
|
+
const tensors = new Map();
|
|
28
|
+
const read = new Set();
|
|
29
|
+
const ignored = [];
|
|
30
|
+
const nodes = [];
|
|
31
|
+
const take = (name, shape) => {
|
|
32
|
+
const tensor = source.get(name);
|
|
33
|
+
if (tensor === undefined) {
|
|
34
|
+
throw new Error(`the source has no weight "${name}", which the definition reads`);
|
|
35
|
+
}
|
|
36
|
+
if (shape !== undefined && shapeText(shape) !== shapeText(tensor.shape)) {
|
|
37
|
+
throw new Error(`"${name}" is ${shapeText(tensor.shape)} in the source and the definition reads it as ` +
|
|
38
|
+
shapeText(shape));
|
|
39
|
+
}
|
|
40
|
+
read.add(name);
|
|
41
|
+
return tensor;
|
|
42
|
+
};
|
|
43
|
+
const weights = {
|
|
44
|
+
read(name, shape) {
|
|
45
|
+
if (!tensors.has(name))
|
|
46
|
+
tensors.set(name, take(name, shape));
|
|
47
|
+
else
|
|
48
|
+
take(name, shape);
|
|
49
|
+
return name;
|
|
50
|
+
},
|
|
51
|
+
derive(name, shape, compute) {
|
|
52
|
+
if (tensors.has(name))
|
|
53
|
+
return name;
|
|
54
|
+
const held = source.get(name);
|
|
55
|
+
const data = held === undefined
|
|
56
|
+
? compute((part, partShape) => take(part, partShape).data)
|
|
57
|
+
: take(name, shape).data;
|
|
58
|
+
if (data.length !== shape.reduce((total, d) => total * d, 1)) {
|
|
59
|
+
throw new Error(`the derived weight "${name}" has ${data.length} values for ${shapeText(shape)}`);
|
|
60
|
+
}
|
|
61
|
+
tensors.set(name, { shape, data });
|
|
62
|
+
return name;
|
|
63
|
+
},
|
|
64
|
+
constant(name, shape, data) {
|
|
65
|
+
const key = `${CONSTANT_PREFIX}${name}`;
|
|
66
|
+
tensors.set(key, { shape, data });
|
|
67
|
+
return key;
|
|
68
|
+
},
|
|
69
|
+
ignore(nameOrPrefix) {
|
|
70
|
+
ignored.push(nameOrPrefix);
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
let count = 0;
|
|
74
|
+
const graph = {
|
|
75
|
+
node(op, inputs, attributes = {}, output = `v${(count += 1)}`) {
|
|
76
|
+
nodes.push({ op, inputs, output, attributes });
|
|
77
|
+
return output;
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
const { inputs, outputs } = architecture(weights, graph);
|
|
81
|
+
const aside = (name) => name.startsWith(CONSTANT_PREFIX) ||
|
|
82
|
+
ignored.some((rule) => (rule.endsWith('.') ? name.startsWith(rule) : name === rule));
|
|
83
|
+
const unread = [...source.names()].filter((name) => !read.has(name) && !aside(name));
|
|
84
|
+
if (unread.length > 0) {
|
|
85
|
+
const named = unread.slice(0, 5).map((name) => `"${name}"`);
|
|
86
|
+
throw new Error(`the definition never reads ${named.join(', ')}` +
|
|
87
|
+
(unread.length > 5 ? ` and ${unread.length - 5} more` : '') +
|
|
88
|
+
': a weight left behind is a network that runs and answers wrongly, so read it or set it ' +
|
|
89
|
+
'aside by name');
|
|
90
|
+
}
|
|
91
|
+
const network = { inputs, outputs, nodes, tensors };
|
|
92
|
+
const problem = validateGraph(network);
|
|
93
|
+
if (problem !== null)
|
|
94
|
+
throw new Error(`the definition's graph cannot run: ${problem}`);
|
|
95
|
+
return network;
|
|
96
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `out[batch][queries][channels]` from `q` of that shape and `k` and `v` of
|
|
3
|
+
* `[batch][keys][channels]`. `scratch` holds at least `queries × keys` values and is overwritten.
|
|
4
|
+
*/
|
|
5
|
+
export declare function attention(out: Float32Array, q: Float32Array, k: Float32Array, v: Float32Array, queries: number, keys: number, channels: number, heads: number, scratch: Float32Array, bias?: Float32Array | null, batch?: number): void;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-head attention, as a composition of the dense operators.
|
|
3
|
+
*
|
|
4
|
+
* **Heads are contiguous runs of channels, in the upstream order**: head `h` of `heads` owns
|
|
5
|
+
* channels `h·d` to `(h+1)·d − 1`, where `d` is the channel count over the head count. A port that
|
|
6
|
+
* interleaves them instead evaluates a network nobody trained.
|
|
7
|
+
*
|
|
8
|
+
* The queries, keys and values arrive already projected — the projections are matrix multiplies a
|
|
9
|
+
* graph states on its own — so this is the part that is attention's alone: scores scaled by
|
|
10
|
+
* `1/√d`, a stable softmax over each query's row, and the weighted sum of values.
|
|
11
|
+
*
|
|
12
|
+
* **A bias is added to the scaled scores**, `[heads][queries][keys]`, as a relative-position table
|
|
13
|
+
* or a mask is upstream; and **a batch is independent windows**, each attending over its own keys
|
|
14
|
+
* with the one bias. Queries and keys may differ in number, as they do where one set of tokens
|
|
15
|
+
* reads another.
|
|
16
|
+
*/
|
|
17
|
+
import { softmax } from './linear.js';
|
|
18
|
+
/**
|
|
19
|
+
* `out[batch][queries][channels]` from `q` of that shape and `k` and `v` of
|
|
20
|
+
* `[batch][keys][channels]`. `scratch` holds at least `queries × keys` values and is overwritten.
|
|
21
|
+
*/
|
|
22
|
+
export function attention(out, q, k, v, queries, keys, channels, heads, scratch, bias = null, batch = 1) {
|
|
23
|
+
const d = channels / heads;
|
|
24
|
+
if (!Number.isInteger(d)) {
|
|
25
|
+
throw new RangeError(`attention: ${channels} channels do not split into ${heads} heads`);
|
|
26
|
+
}
|
|
27
|
+
const scale = 1 / Math.sqrt(d);
|
|
28
|
+
for (let b = 0; b < batch; b += 1) {
|
|
29
|
+
const qAt = b * queries * channels;
|
|
30
|
+
const kAt = b * keys * channels;
|
|
31
|
+
for (let head = 0; head < heads; head += 1) {
|
|
32
|
+
const first = head * d;
|
|
33
|
+
for (let query = 0; query < queries; query += 1) {
|
|
34
|
+
for (let key = 0; key < keys; key += 1) {
|
|
35
|
+
let dot = 0;
|
|
36
|
+
for (let c = 0; c < d; c += 1) {
|
|
37
|
+
dot +=
|
|
38
|
+
q[qAt + query * channels + first + c] *
|
|
39
|
+
k[kAt + key * channels + first + c];
|
|
40
|
+
}
|
|
41
|
+
const at = query * keys + key;
|
|
42
|
+
scratch[at] = dot * scale;
|
|
43
|
+
if (bias !== null) {
|
|
44
|
+
scratch[at] = scratch[at] + bias[head * queries * keys + at];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
softmax(scratch, queries, keys, scratch);
|
|
49
|
+
for (let query = 0; query < queries; query += 1) {
|
|
50
|
+
for (let c = 0; c < d; c += 1) {
|
|
51
|
+
let sum = 0;
|
|
52
|
+
for (let key = 0; key < keys; key += 1) {
|
|
53
|
+
sum +=
|
|
54
|
+
scratch[query * keys + key] *
|
|
55
|
+
v[kAt + key * channels + first + c];
|
|
56
|
+
}
|
|
57
|
+
out[qAt + query * channels + first + c] = sum;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/** The dense operators' rows: projections, activations, normalisation, attention. */
|
|
2
|
+
import { attention } from './attention.js';
|
|
3
|
+
import { gelu, layerNorm, sigmoid, softmax } from './linear.js';
|
|
4
|
+
import { elementwise, num, product, same } from './operatorKit.js';
|
|
5
|
+
export const DENSE_OPERATORS = [
|
|
6
|
+
[
|
|
7
|
+
'linear',
|
|
8
|
+
{
|
|
9
|
+
ranks: [2, 2, 1],
|
|
10
|
+
arity: [2, 3],
|
|
11
|
+
shape: ([x, w, b]) => {
|
|
12
|
+
const [rows, width] = x;
|
|
13
|
+
const [outs, ins] = w;
|
|
14
|
+
if (width !== ins)
|
|
15
|
+
return `x has ${width} columns and the weight takes ${ins}`;
|
|
16
|
+
if (b !== undefined && b[0] !== outs)
|
|
17
|
+
return `the bias has ${b[0]} and the weight ${outs}`;
|
|
18
|
+
return [rows, outs];
|
|
19
|
+
},
|
|
20
|
+
evaluate: ([x, w, b], [xs, ws], _attributes, out) => {
|
|
21
|
+
const [rows, ins] = xs;
|
|
22
|
+
const outs = ws[0];
|
|
23
|
+
for (let row = 0; row < rows; row += 1) {
|
|
24
|
+
for (let o = 0; o < outs; o += 1) {
|
|
25
|
+
let dot = 0;
|
|
26
|
+
for (let i = 0; i < ins; i += 1) {
|
|
27
|
+
dot +=
|
|
28
|
+
x[row * ins + i] *
|
|
29
|
+
w[o * ins + i];
|
|
30
|
+
}
|
|
31
|
+
/* The bias joins the double-precision sum, and the result is rounded once. */
|
|
32
|
+
out[row * outs + o] = dot + (b === undefined ? 0 : b[o]);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
[
|
|
39
|
+
'relu',
|
|
40
|
+
{
|
|
41
|
+
arity: [1, 1],
|
|
42
|
+
shape: ([x]) => [...x],
|
|
43
|
+
evaluate: ([x], _shapes, _attributes, out) => {
|
|
44
|
+
for (let i = 0; i < out.length; i += 1)
|
|
45
|
+
out[i] = Math.max(0, x[i]);
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
[
|
|
50
|
+
'gelu',
|
|
51
|
+
{
|
|
52
|
+
arity: [1, 1],
|
|
53
|
+
shape: ([x]) => [...x],
|
|
54
|
+
evaluate: ([x], _shapes, _attributes, out) => gelu(x, out),
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
[
|
|
58
|
+
'sigmoid',
|
|
59
|
+
{
|
|
60
|
+
arity: [1, 1],
|
|
61
|
+
shape: ([x]) => [...x],
|
|
62
|
+
evaluate: ([x], _shapes, _attributes, out) => sigmoid(x, out),
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
['add', elementwise((a, b) => a + b)],
|
|
66
|
+
['mul', elementwise((a, b) => a * b)],
|
|
67
|
+
[
|
|
68
|
+
'layerNorm',
|
|
69
|
+
{
|
|
70
|
+
ranks: [2, 1, 1],
|
|
71
|
+
arity: [3, 3],
|
|
72
|
+
shape: ([x, gamma]) => {
|
|
73
|
+
const cols = x[1];
|
|
74
|
+
return gamma?.[0] === cols ? [...x] : `gamma has ${gamma?.[0]} and x ${cols}`;
|
|
75
|
+
},
|
|
76
|
+
evaluate: ([x, gamma, beta], [xs], attributes, out) => {
|
|
77
|
+
const [rows, cols] = xs;
|
|
78
|
+
const epsilon = num(attributes, 'epsilon', 1e-6);
|
|
79
|
+
layerNorm(x, rows, cols, gamma, beta, epsilon, out);
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
[
|
|
84
|
+
'softmax',
|
|
85
|
+
{
|
|
86
|
+
arity: [1, 1],
|
|
87
|
+
shape: ([x]) => [...x],
|
|
88
|
+
evaluate: ([x], [xs], _attributes, out) => {
|
|
89
|
+
const shape = xs;
|
|
90
|
+
const cols = shape[shape.length - 1];
|
|
91
|
+
softmax(x, product(shape) / cols, cols, out);
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
[
|
|
96
|
+
'attention',
|
|
97
|
+
{
|
|
98
|
+
/*
|
|
99
|
+
* Rank 2, or 3 for a batch of windows; checked here rather than by `ranks`, which states one.
|
|
100
|
+
* The bias, where there is one, is a score for each head, query and key.
|
|
101
|
+
*/
|
|
102
|
+
arity: [3, 4],
|
|
103
|
+
shape: ([q, k, v, b], attributes) => {
|
|
104
|
+
const qs = q;
|
|
105
|
+
const ks = k;
|
|
106
|
+
if (qs.length !== 2 && qs.length !== 3)
|
|
107
|
+
return `the queries have rank ${qs.length}, not 2 or 3`;
|
|
108
|
+
if (ks.length !== qs.length)
|
|
109
|
+
return `the keys have rank ${ks.length} and the queries ${qs.length}`;
|
|
110
|
+
if (!same(ks, v))
|
|
111
|
+
return 'the keys and values differ in shape';
|
|
112
|
+
if (qs.length === 3 && qs[0] !== ks[0]) {
|
|
113
|
+
return `a batch of ${qs[0]} queries and of ${ks[0]} keys`;
|
|
114
|
+
}
|
|
115
|
+
const [queries, channels] = qs.slice(-2);
|
|
116
|
+
const [keys, keyChannels] = ks.slice(-2);
|
|
117
|
+
if (channels !== keyChannels) {
|
|
118
|
+
return `the queries have ${channels} channels and the keys ${keyChannels}`;
|
|
119
|
+
}
|
|
120
|
+
const heads = num(attributes, 'heads', 1);
|
|
121
|
+
if (channels % heads !== 0)
|
|
122
|
+
return `${channels} channels do not split into ${heads} heads`;
|
|
123
|
+
if (b !== undefined && !same(b, [heads, queries, keys])) {
|
|
124
|
+
return `the bias is [${b.join(', ')}], not [${heads}, ${queries}, ${keys}]`;
|
|
125
|
+
}
|
|
126
|
+
return [...qs];
|
|
127
|
+
},
|
|
128
|
+
scratch: ([q, k]) => q.at(-2) * k.at(-2),
|
|
129
|
+
evaluate: ([q, k, v, b], [qs, ks], attributes, out, scratch) => {
|
|
130
|
+
const shape = qs;
|
|
131
|
+
const [queries, channels] = shape.slice(-2);
|
|
132
|
+
attention(out, q, k, v, queries, ks.at(-2), channels, num(attributes, 'heads', 1), scratch, b ?? null, shape.length === 3 ? shape[0] : 1);
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
];
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { type Attributes } from './operators.ts';
|
|
2
|
+
export interface GraphValue {
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly shape: readonly number[];
|
|
5
|
+
}
|
|
6
|
+
export interface GraphNode {
|
|
7
|
+
readonly op: string;
|
|
8
|
+
readonly inputs: readonly string[];
|
|
9
|
+
readonly output: string;
|
|
10
|
+
readonly attributes: Attributes;
|
|
11
|
+
}
|
|
12
|
+
export interface GraphTensor {
|
|
13
|
+
readonly shape: readonly number[];
|
|
14
|
+
readonly data: Float32Array;
|
|
15
|
+
}
|
|
16
|
+
export interface NetworkGraph {
|
|
17
|
+
readonly inputs: readonly GraphValue[];
|
|
18
|
+
readonly outputs: readonly string[];
|
|
19
|
+
readonly nodes: readonly GraphNode[];
|
|
20
|
+
readonly tensors: ReadonlyMap<string, GraphTensor>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A graph as a file stores it: its tensors in either precision, half as the bits a device uploads.
|
|
24
|
+
* `@driftengine/drft`'s `DrftGraph` is one, structurally, which is how the two packages meet
|
|
25
|
+
* without either importing the other.
|
|
26
|
+
*/
|
|
27
|
+
export interface StoredGraph {
|
|
28
|
+
readonly inputs: readonly GraphValue[];
|
|
29
|
+
readonly outputs: readonly string[];
|
|
30
|
+
readonly nodes: readonly GraphNode[];
|
|
31
|
+
readonly tensors: readonly {
|
|
32
|
+
readonly name: string;
|
|
33
|
+
readonly shape: readonly number[];
|
|
34
|
+
readonly data: Float32Array | Uint16Array;
|
|
35
|
+
}[];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A stored graph as one the runtime runs, half-precision tensors decoded to single; validated, and
|
|
39
|
+
* refused naming what is wrong — an operator the runtime lacks is a fact about the model, and this
|
|
40
|
+
* is when a caller learns it.
|
|
41
|
+
*/
|
|
42
|
+
export declare function graphFromStored(stored: StoredGraph): NetworkGraph;
|
|
43
|
+
/** Every value's shape — inputs, tensors and intermediates — refused, with the reason, if invalid. */
|
|
44
|
+
export declare function graphShapes(graph: NetworkGraph): ReadonlyMap<string, readonly number[]>;
|
|
45
|
+
/**
|
|
46
|
+
* A graph as `@driftengine/core`'s runner takes it: its tensors listed by name, and every value's
|
|
47
|
+
* shape as inferred here — **nothing evaluated and nothing sized**, where asking an evaluator for its
|
|
48
|
+
* shapes would allocate every intermediate value first. Structural, because core does not import
|
|
49
|
+
* this package; the shapes are this module's, so there is one copy of every operator's rule.
|
|
50
|
+
*/
|
|
51
|
+
export declare function graphForDevice(graph: NetworkGraph): {
|
|
52
|
+
readonly inputs: readonly GraphValue[];
|
|
53
|
+
readonly outputs: readonly string[];
|
|
54
|
+
readonly nodes: readonly GraphNode[];
|
|
55
|
+
readonly tensors: readonly {
|
|
56
|
+
readonly name: string;
|
|
57
|
+
readonly shape: readonly number[];
|
|
58
|
+
readonly data: Float32Array;
|
|
59
|
+
}[];
|
|
60
|
+
readonly shapes: ReadonlyMap<string, readonly number[]>;
|
|
61
|
+
};
|
|
62
|
+
/** Null when the graph can run; otherwise the reason, naming what it is about. */
|
|
63
|
+
export declare function validateGraph(graph: NetworkGraph): string | null;
|
|
64
|
+
export interface GraphEvaluator {
|
|
65
|
+
/** Every value's shape, inputs, tensors and intermediates alike. */
|
|
66
|
+
readonly shapes: ReadonlyMap<string, readonly number[]>;
|
|
67
|
+
/**
|
|
68
|
+
* Evaluate with these inputs. The returned arrays are the evaluator's own and are overwritten by
|
|
69
|
+
* the next run.
|
|
70
|
+
*/
|
|
71
|
+
run(inputs: ReadonlyMap<string, Float32Array>): Map<string, Float32Array>;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Validate, size every value once, and return something that evaluates without allocating.
|
|
75
|
+
*
|
|
76
|
+
* **Values that cannot be alive at once share a buffer**, by `planReuse` — the rule the device's
|
|
77
|
+
* runner plans by too. Weights and inputs stay in the caller's own arrays and are planned by
|
|
78
|
+
* neither. What it buys is the difference between holding every value of a graph and holding the
|
|
79
|
+
* few alive at any moment: OWLv2's image graph at 960² is 3,647 MB of values and 196 MB in ten buffers,
|
|
80
|
+
* which is the difference between a reference that runs on an ordinary machine and one that is
|
|
81
|
+
* killed for its memory.
|
|
82
|
+
*/
|
|
83
|
+
export declare function createGraphEvaluator(graph: NetworkGraph): GraphEvaluator;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A network as a graph of the runtime's operators, checked before it runs and evaluated on the CPU.
|
|
3
|
+
*
|
|
4
|
+
* **Validation says what is wrong in words, and names what it is about**: an operator the runtime
|
|
5
|
+
* lacks, a value read before anything writes it, a shape that does not fit the node that reads it,
|
|
6
|
+
* an output nothing produces. A graph is checked when it is loaded — a model is refused then, with
|
|
7
|
+
* the reason, rather than at its first frame with a picture of zeros.
|
|
8
|
+
*
|
|
9
|
+
* **The evaluator is the reference**, which the device runner is held to: it sizes every value and
|
|
10
|
+
* its scratch once, when it is created, and a run writes into those buffers and allocates nothing.
|
|
11
|
+
* What it gives up is memory: every intermediate value keeps its own buffer, which is the plainest
|
|
12
|
+
* thing to compare against and the device runner's job to improve on.
|
|
13
|
+
*/
|
|
14
|
+
import { fromHalfBits } from '../half.js';
|
|
15
|
+
import { OPERATORS } from './operators.js';
|
|
16
|
+
import { planReuse } from '@driftengine/core';
|
|
17
|
+
const size = (shape) => shape.reduce((total, d) => total * d, 1);
|
|
18
|
+
/** Every value's shape, or the first reason the graph cannot run. */
|
|
19
|
+
function inferShapes(graph) {
|
|
20
|
+
const shapes = new Map();
|
|
21
|
+
for (const input of graph.inputs)
|
|
22
|
+
shapes.set(input.name, input.shape);
|
|
23
|
+
for (const [name, tensor] of graph.tensors) {
|
|
24
|
+
if (tensor.data.length !== size(tensor.shape)) {
|
|
25
|
+
return `tensor "${name}" holds ${tensor.data.length} values and its shape [${tensor.shape.join(', ')}] needs ${size(tensor.shape)}`;
|
|
26
|
+
}
|
|
27
|
+
shapes.set(name, tensor.shape);
|
|
28
|
+
}
|
|
29
|
+
for (const node of graph.nodes) {
|
|
30
|
+
const operator = OPERATORS.get(node.op);
|
|
31
|
+
if (operator === undefined) {
|
|
32
|
+
return `the runtime has no operator "${node.op}" (writing "${node.output}")`;
|
|
33
|
+
}
|
|
34
|
+
const [fewest, most] = operator.arity;
|
|
35
|
+
if (node.inputs.length < fewest || node.inputs.length > most) {
|
|
36
|
+
return `${node.op} writing "${node.output}" takes ${fewest} to ${most} inputs and was given ${node.inputs.length}`;
|
|
37
|
+
}
|
|
38
|
+
const inputShapes = [];
|
|
39
|
+
for (const input of node.inputs) {
|
|
40
|
+
const shape = shapes.get(input);
|
|
41
|
+
if (shape === undefined)
|
|
42
|
+
return `${node.op} writing "${node.output}" reads "${input}", which nothing writes`;
|
|
43
|
+
inputShapes.push(shape);
|
|
44
|
+
}
|
|
45
|
+
if (shapes.has(node.output))
|
|
46
|
+
return `"${node.output}" is written twice`;
|
|
47
|
+
const ranks = operator.ranks ?? [];
|
|
48
|
+
for (let i = 0; i < inputShapes.length && i < ranks.length; i += 1) {
|
|
49
|
+
const rank = inputShapes[i].length;
|
|
50
|
+
if (rank !== ranks[i]) {
|
|
51
|
+
return `${node.op} writing "${node.output}": "${node.inputs[i]}" has rank ${rank} and this input takes rank ${ranks[i]}`;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const shape = operator.shape(inputShapes, node.attributes);
|
|
55
|
+
if (typeof shape === 'string')
|
|
56
|
+
return `${node.op} writing "${node.output}": ${shape}`;
|
|
57
|
+
shapes.set(node.output, shape);
|
|
58
|
+
}
|
|
59
|
+
for (const output of graph.outputs) {
|
|
60
|
+
if (!shapes.has(output))
|
|
61
|
+
return `the output "${output}" is written by nothing`;
|
|
62
|
+
}
|
|
63
|
+
return shapes;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* A stored graph as one the runtime runs, half-precision tensors decoded to single; validated, and
|
|
67
|
+
* refused naming what is wrong — an operator the runtime lacks is a fact about the model, and this
|
|
68
|
+
* is when a caller learns it.
|
|
69
|
+
*/
|
|
70
|
+
export function graphFromStored(stored) {
|
|
71
|
+
const tensors = new Map();
|
|
72
|
+
for (const tensor of stored.tensors) {
|
|
73
|
+
const data = tensor.data instanceof Uint16Array
|
|
74
|
+
? Float32Array.from(tensor.data, (bits) => fromHalfBits(bits))
|
|
75
|
+
: tensor.data;
|
|
76
|
+
tensors.set(tensor.name, { shape: tensor.shape, data });
|
|
77
|
+
}
|
|
78
|
+
const graph = {
|
|
79
|
+
inputs: stored.inputs,
|
|
80
|
+
outputs: stored.outputs,
|
|
81
|
+
nodes: stored.nodes,
|
|
82
|
+
tensors,
|
|
83
|
+
};
|
|
84
|
+
const problem = validateGraph(graph);
|
|
85
|
+
if (problem !== null)
|
|
86
|
+
throw new Error(`network graph: ${problem}`);
|
|
87
|
+
return graph;
|
|
88
|
+
}
|
|
89
|
+
/** Every value's shape — inputs, tensors and intermediates — refused, with the reason, if invalid. */
|
|
90
|
+
export function graphShapes(graph) {
|
|
91
|
+
const shapes = inferShapes(graph);
|
|
92
|
+
if (typeof shapes === 'string')
|
|
93
|
+
throw new Error(`network graph: ${shapes}`);
|
|
94
|
+
return shapes;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* A graph as `@driftengine/core`'s runner takes it: its tensors listed by name, and every value's
|
|
98
|
+
* shape as inferred here — **nothing evaluated and nothing sized**, where asking an evaluator for its
|
|
99
|
+
* shapes would allocate every intermediate value first. Structural, because core does not import
|
|
100
|
+
* this package; the shapes are this module's, so there is one copy of every operator's rule.
|
|
101
|
+
*/
|
|
102
|
+
export function graphForDevice(graph) {
|
|
103
|
+
return {
|
|
104
|
+
inputs: graph.inputs,
|
|
105
|
+
outputs: graph.outputs,
|
|
106
|
+
nodes: graph.nodes,
|
|
107
|
+
tensors: [...graph.tensors].map(([name, tensor]) => ({ name, ...tensor })),
|
|
108
|
+
shapes: graphShapes(graph),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/** Null when the graph can run; otherwise the reason, naming what it is about. */
|
|
112
|
+
export function validateGraph(graph) {
|
|
113
|
+
const shapes = inferShapes(graph);
|
|
114
|
+
return typeof shapes === 'string' ? shapes : null;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Validate, size every value once, and return something that evaluates without allocating.
|
|
118
|
+
*
|
|
119
|
+
* **Values that cannot be alive at once share a buffer**, by `planReuse` — the rule the device's
|
|
120
|
+
* runner plans by too. Weights and inputs stay in the caller's own arrays and are planned by
|
|
121
|
+
* neither. What it buys is the difference between holding every value of a graph and holding the
|
|
122
|
+
* few alive at any moment: OWLv2's image graph at 960² is 3,647 MB of values and 196 MB in ten buffers,
|
|
123
|
+
* which is the difference between a reference that runs on an ordinary machine and one that is
|
|
124
|
+
* killed for its memory.
|
|
125
|
+
*/
|
|
126
|
+
export function createGraphEvaluator(graph) {
|
|
127
|
+
const shapes = inferShapes(graph);
|
|
128
|
+
if (typeof shapes === 'string')
|
|
129
|
+
throw new Error(`network graph: ${shapes}`);
|
|
130
|
+
const buffers = new Map();
|
|
131
|
+
for (const [name, tensor] of graph.tensors)
|
|
132
|
+
buffers.set(name, tensor.data);
|
|
133
|
+
const plan = planReuse({ nodes: graph.nodes, held: [], outputs: graph.outputs }, (name) => size(shapes.get(name)));
|
|
134
|
+
const slots = plan.sizes.map((values) => new Float32Array(values));
|
|
135
|
+
/* A buffer is as large as the largest value it holds, so each value takes a view of its own. */
|
|
136
|
+
for (const [name, slot] of plan.slotOf) {
|
|
137
|
+
const room = size(shapes.get(name));
|
|
138
|
+
const held = slots[slot];
|
|
139
|
+
buffers.set(name, held.length === room ? held : held.subarray(0, room));
|
|
140
|
+
}
|
|
141
|
+
let scratchSize = 0;
|
|
142
|
+
for (const node of graph.nodes) {
|
|
143
|
+
const operator = OPERATORS.get(node.op);
|
|
144
|
+
const inputShapes = node.inputs.map((input) => shapes.get(input));
|
|
145
|
+
scratchSize = Math.max(scratchSize, operator?.scratch?.(inputShapes, node.attributes) ?? 0);
|
|
146
|
+
}
|
|
147
|
+
const scratch = new Float32Array(scratchSize);
|
|
148
|
+
const outputs = new Map();
|
|
149
|
+
return {
|
|
150
|
+
shapes,
|
|
151
|
+
run(inputs) {
|
|
152
|
+
for (const input of graph.inputs) {
|
|
153
|
+
const data = inputs.get(input.name);
|
|
154
|
+
if (data === undefined)
|
|
155
|
+
throw new Error(`network graph: no value for the input "${input.name}"`);
|
|
156
|
+
if (data.length !== size(input.shape)) {
|
|
157
|
+
throw new Error(`network graph: "${input.name}" holds ${data.length} values and needs ${size(input.shape)}`);
|
|
158
|
+
}
|
|
159
|
+
buffers.set(input.name, data);
|
|
160
|
+
}
|
|
161
|
+
for (const node of graph.nodes) {
|
|
162
|
+
const operator = OPERATORS.get(node.op);
|
|
163
|
+
if (operator === undefined)
|
|
164
|
+
continue;
|
|
165
|
+
const values = node.inputs.map((input) => buffers.get(input));
|
|
166
|
+
const inputShapes = node.inputs.map((input) => shapes.get(input));
|
|
167
|
+
operator.evaluate(values, inputShapes, node.attributes, buffers.get(node.output), scratch);
|
|
168
|
+
}
|
|
169
|
+
outputs.clear();
|
|
170
|
+
for (const output of graph.outputs)
|
|
171
|
+
outputs.set(output, buffers.get(output));
|
|
172
|
+
return outputs;
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dense operators a transformer is built from: matrix multiply, bias, layer norm, GELU, softmax.
|
|
3
|
+
*
|
|
4
|
+
* **The references the device kernels are held to**, which is why they are written for exactness
|
|
5
|
+
* rather than speed: every sum accumulates in double precision and is rounded once, on the way into
|
|
6
|
+
* the caller's single-precision output. A kernel that disagrees with one of these is wrong, not
|
|
7
|
+
* this.
|
|
8
|
+
*
|
|
9
|
+
* **Contiguous row-major arrays with their dimensions, and nothing that allocates.** A strided view
|
|
10
|
+
* that permutes without copying would make every operator handle strides, and every device kernel
|
|
11
|
+
* would materialise the permutation anyway; so a permutation is a copy into a buffer the caller
|
|
12
|
+
* owns, and the operators stay one loop each.
|
|
13
|
+
*
|
|
14
|
+
* **The exact GELU, `x·Φ(x)`, and never the tanh approximation.** The two differ by 4e-4 at 3, and
|
|
15
|
+
* a port of a network trained with one and evaluated with the other is a different network. `erf`
|
|
16
|
+
* is here for that reason, accurate to double precision.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* `out[m×n] = a[m×k] · b`, where `b` is `k×n`, or `n×k` read as its transpose when `transposeB`.
|
|
20
|
+
* The offsets are where each matrix starts in its array, which is how one head of many is taken.
|
|
21
|
+
*/
|
|
22
|
+
export declare function matmul(out: Float32Array, a: Float32Array, b: Float32Array, m: number, k: number, n: number, transposeB?: boolean, outAt?: number, aAt?: number, bAt?: number): void;
|
|
23
|
+
/** Add `bias[cols]` to every one of `rows` rows of `x`, in place. */
|
|
24
|
+
export declare function addBias(x: Float32Array, rows: number, cols: number, bias: Float32Array): void;
|
|
25
|
+
/**
|
|
26
|
+
* Each row normalised to zero mean and unit variance, then scaled by `gamma` and shifted by `beta`.
|
|
27
|
+
*
|
|
28
|
+
* **The population variance, divided by the row's length and not one less**, which is what the
|
|
29
|
+
* upstream frameworks compute and therefore what their trained weights expect.
|
|
30
|
+
*/
|
|
31
|
+
export declare function layerNorm(x: Float32Array, rows: number, cols: number, gamma: Float32Array, beta: Float32Array, epsilon: number, out: Float32Array): void;
|
|
32
|
+
/**
|
|
33
|
+
* The error function, to double precision.
|
|
34
|
+
*
|
|
35
|
+
* A Maclaurin series below 3, where its alternating terms peak near 170 at `x = 3` and cost about
|
|
36
|
+
* three digits to cancellation — still 1e-13; a continued fraction for the complement above, which
|
|
37
|
+
* converges fastest exactly where the series is worst; and ±1 past 6, where the complement is below
|
|
38
|
+
* a double's last place.
|
|
39
|
+
*/
|
|
40
|
+
export declare function erf(x: number): number;
|
|
41
|
+
/** `x·Φ(x)`, the exact GELU, from `x` into `out`. */
|
|
42
|
+
export declare function gelu(x: Float32Array, out: Float32Array): void;
|
|
43
|
+
/** The logistic, `1/(1 + e^−x)`, from `x` into `out`: 1 far to the right, and 0 far to the left. */
|
|
44
|
+
export declare function sigmoid(x: Float32Array, out: Float32Array): void;
|
|
45
|
+
/**
|
|
46
|
+
* Each row of `x` to a distribution, stably: the row's largest value is subtracted before the
|
|
47
|
+
* exponential, so logits near a thousand give their logistic rather than `Infinity / Infinity`.
|
|
48
|
+
*/
|
|
49
|
+
export declare function softmax(x: Float32Array, rows: number, cols: number, out: Float32Array): void;
|