minicpm5-2b-webgpu / src /dspark-runtime.js
ProCreations's picture
Dedicated WebGPU runtime, DSpark acceleration, and automatic tool repair
c51a3e7 verified
Raw History Blame Contribute Delete
14.6 kB
import { MiniCPMRuntime, layoutTypes, cacheBytes } from "./minicpm-runtime.js";
import { buildDSparkKernels } from "./dspark-kernels.js";
import {
H,
F,
KV,
VOCAB,
MAX_CHUNKS,
PREFILL_BATCH,
} from "./minicpm-kernels.js";
export const DSPARK_LAYERS = [1, 10, 20, 30, 39];
const extraTypes = {
projectBatch: ["r", "r", "w"],
projectBlock: ["r", "r", "w"],
contextBatch: ["r", "r", "w"],
contextBlock: ["r", "r", "w"],
capture: ["r", "r", "w", "u"],
noiseNormRope: ["r", "r", "r", "w", "w", "w"],
contextNormRope: ["r", "r", "r", "w", "w", "w"],
noise: ["r", "w"],
markovEmbed: ["r", "r", "w"],
markov: ["r", "r", "w"],
addBias: ["r", "r", "w"],
argmax: ["r", "w"],
argmaxFinal: ["r", "w"],
};
export class DSparkRuntime extends MiniCPMRuntime {
async initialize(parent, blob, signal) {
this.parent = parent;
this.device = parent.device;
this.adapter = parent.adapter;
this.subgroups = parent.subgroups;
this.maxContext = parent.maxContext;
this.stepLayout = parent.stepLayout;
this.settings = parent.settings;
const all = buildDSparkKernels({
subgroups: this.subgroups,
kv: this.settings.cacheK,
});
const needed = [
"norm",
"qkvBlock",
"outputBlock",
"upGateBlock",
"downBlock",
"silu",
"projectBatch",
"projectBlock",
"contextBatch",
"contextBlock",
"markov",
"store",
"attention",
"combine",
...Object.keys(extraTypes),
];
await Promise.all(
[...new Set(needed)].map(async (name) => {
const module = this.device.createShaderModule({
code: all[name],
label: "dspark " + name,
});
const info = await module.getCompilationInfo();
const errors = info.messages.filter((m) => m.type === "error");
if (errors.length)
throw new Error(
"DSpark " +
name +
": " +
errors.map((e) => e.message + " at " + e.lineNum).join("; "),
);
const types =
extraTypes[name] ||
layoutTypes[name] ||
layoutTypes[name.replace(/Block$/, "")];
const layout = this.device.createBindGroupLayout({
entries: types.map((type, binding) => ({
binding,
visibility: GPUShaderStage.COMPUTE,
buffer: {
type:
type === "u"
? "uniform"
: type === "w"
? "storage"
: "read-only-storage",
},
})),
});
this.layouts[name] = layout;
this.pipelines[name] = await this.device.createComputePipelineAsync({
layout: this.device.createPipelineLayout({
bindGroupLayouts: [this.stepLayout, layout],
}),
compute: { module, entryPoint: "main" },
label: "DSpark " + name,
});
}),
);
const headSize = Number(
new DataView(await blob.slice(0, 8).arrayBuffer()).getBigUint64(0, true),
);
if (headSize > 1024 * 1024)
throw new Error("Invalid DSpark weights header.");
const header = JSON.parse(await blob.slice(8, 8 + headSize).text());
const offset = 8 + headSize;
const upload = async (names, label, fp32 = false) => {
const list = [];
for (const name of names) {
signal?.throwIfAborted();
const item = header[name];
if (!item || item.dtype !== "BF16")
throw new Error("Missing BF16 DSpark tensor: " + name);
const bytes = await blob
.slice(offset + item.data_offsets[0], offset + item.data_offsets[1])
.arrayBuffer();
if (fp32) {
const source = new Uint16Array(bytes),
out = new Uint32Array(source.length);
for (let i = 0; i < source.length; i++) out[i] = source[i] << 16;
list.push(out.buffer);
} else list.push(bytes);
}
const b = this.buffer(
list.reduce((n, a) => n + Math.ceil(a.byteLength / 4) * 4, 0),
label,
);
let at = 0;
for (const bytes of list) {
let data = bytes;
if (data.byteLength % 4) {
const padded = new Uint8Array(Math.ceil(data.byteLength / 4) * 4);
padded.set(new Uint8Array(data));
data = padded.buffer;
}
this.device.queue.writeBuffer(b, at, data);
at += data.byteLength;
}
return b;
};
const w = (this.work = {});
for (const [name, count] of Object.entries({
features: PREFILL_BATCH * H * 5,
projected: PREFILL_BATCH * H,
context: PREFILL_BATCH * H,
contextKV: PREFILL_BATCH * KV * 2,
x: PREFILL_BATCH * H,
norm: PREFILL_BATCH * H,
temp: PREFILL_BATCH * H,
zeros: PREFILL_BATCH * H,
qkv: PREFILL_BATCH * (H + 2 * KV),
q: PREFILL_BATCH * H,
k: PREFILL_BATCH * KV,
v: PREFILL_BATCH * KV,
attention: PREFILL_BATCH * H,
upGate: PREFILL_BATCH * F * 2,
hidden: PREFILL_BATCH * F,
partial: PREFILL_BATCH * 16 * MAX_CHUNKS * 128,
md: PREFILL_BATCH * 16 * MAX_CHUNKS * 2,
tokens: this.maxContext + 9,
baseLogits: 8 * VOCAB,
bias: VOCAB,
logits: VOCAB,
markov: 256,
argmax: 256,
}))
w[name] = this.buffer(count * 4, "DSpark " + name);
w.sampler = this.uniform(new Float32Array(4), "draft sampler");
w.steps = this.buffer(
(this.maxContext + 9) * 256,
"DSpark steps",
GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
);
w.sampleSteps = this.buffer(
(this.maxContext + 9) * 256,
"DSpark sample steps",
GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
);
this.stepGroup = this.device.createBindGroup({
layout: this.stepLayout,
entries: [{ binding: 0, resource: { buffer: w.steps, size: 16 } }],
});
this.sampleStepGroup = this.device.createBindGroup({
layout: this.stepLayout,
entries: [{ binding: 0, resource: { buffer: w.sampleSteps, size: 16 } }],
});
const [fc, hiddenNorm, finalNorm, w1, w2, confidence, confidenceBias] =
await Promise.all([
upload(["fc.weight"], "DSpark projection"),
upload(["hidden_norm.weight"], "DSpark hidden norm", true),
upload(["norm.weight"], "DSpark output norm", true),
upload(["markov_head.markov_w1.weight"], "DSpark Markov embeddings"),
upload(["markov_head.markov_w2.weight"], "DSpark Markov projection"),
upload(["confidence_head.proj.weight"], "DSpark confidence", true),
upload(["confidence_head.proj.bias"], "DSpark confidence bias", true),
]);
this.confidence = { weight: confidence, bias: confidenceBias };
this.groups = {
projectBatch: this.group("projectBatch", [w.features, fc, w.projected]),
projectBlock: this.group("projectBlock", [w.features, fc, w.projected]),
hiddenNorm: this.group("norm", [
w.projected,
w.zeros,
hiddenNorm,
w.context,
]),
noise: this.group("noise", [parent.work.tokens, w.tokens]),
embed: parent.group("embed", [...parent.embedding, w.tokens, w.x]),
finalNorm: this.group("norm", [w.x, w.temp, finalNorm, w.norm]),
head: parent.group("headBlock", [w.norm, ...parent.head, w.baseLogits]),
markovEmbed: this.group("markovEmbed", [w1, w.tokens, w.markov]),
markov: this.group("markov", [w.markov, w2, w.bias]),
argmax: this.group("argmax", [w.logits, w.argmax]),
argmaxFinal: this.group("argmaxFinal", [w.argmax, w.tokens]),
};
this.biasGroups = Array.from({ length: 7 }, (_, i) =>
this.group("addBias", [
{ buffer: w.baseLogits, offset: i * VOCAB * 4, size: VOCAB * 4 },
w.bias,
w.logits,
]),
);
this.captureGroups = DSPARK_LAYERS.map((_, i) =>
this.group("capture", [
parent.work.x,
parent.work.temp,
w.features,
this.uniform(new Uint32Array([i, 0, 0, 0]), "capture layer"),
]),
);
for (let i = 0; i < 5; i++) {
const prefix = "layers." + i + ".";
const get = (names, label, float = false) =>
upload(
names.map((n) => prefix + n + ".weight"),
label + " " + i,
float,
);
const [norm, post, qkv, contextKV, o, upGate, down, qNorm, kNorm] =
await Promise.all([
get(["input_layernorm"], "DSpark norm", true),
get(["post_attention_layernorm"], "DSpark post norm", true),
get(
["self_attn.q_proj", "self_attn.k_proj", "self_attn.v_proj"],
"DSpark qkv",
),
get(["self_attn.k_proj", "self_attn.v_proj"], "DSpark context KV"),
get(["self_attn.o_proj"], "DSpark attention out"),
get(["mlp.gate_proj", "mlp.up_proj"], "DSpark gate/up"),
get(["mlp.down_proj"], "DSpark down"),
get(["self_attn.q_norm"], "DSpark Q norm", true),
get(["self_attn.k_norm"], "DSpark K norm", true),
]);
const k = this.buffer(
cacheBytes(this.maxContext, this.settings.cacheK),
"DSpark key cache",
),
v = this.buffer(
cacheBytes(this.maxContext, this.settings.cacheK),
"DSpark value cache",
);
const groups = {
input: this.group("norm", [w.x, i ? w.temp : w.zeros, norm, w.norm]),
post: this.group("norm", [w.x, w.temp, post, w.norm]),
qkv: this.group("qkvBlock", [w.norm, qkv, w.qkv]),
contextBatch: this.group("contextBatch", [
w.context,
contextKV,
w.contextKV,
]),
contextBlock: this.group("contextBlock", [
w.context,
contextKV,
w.contextKV,
]),
noiseRope: this.group("noiseNormRope", [
w.qkv,
qNorm,
kNorm,
w.q,
w.k,
w.v,
]),
contextRope: this.group("contextNormRope", [
w.contextKV,
qNorm,
kNorm,
w.q,
w.k,
w.v,
]),
store: this.group("store", [w.k, w.v, k, v]),
attention: this.group("attention", [w.q, k, v, w.partial, w.md]),
combine: this.group("combine", [w.partial, w.md, w.attention]),
output: this.group("outputBlock", [w.attention, o, w.temp]),
upGate: this.group("upGateBlock", [w.norm, upGate, w.upGate]),
silu: this.group("silu", [w.upGate, w.hidden]),
down: this.group("downBlock", [w.hidden, down, w.temp]),
};
this.layers.push({ groups, k, v });
await this.device.queue.onSubmittedWorkDone();
parent.check();
}
this.sortGroups = {};
for (const keep of [64, 256]) {
const pg = parent.sortGroups[keep];
this.sortGroups[keep] = {
...pg,
first: parent.group("sortFirst" + keep, [
w.logits,
parent.work.sortA,
w.tokens,
parent.work.penalty,
]),
sample: parent.group("sample", [pg.source, w.tokens, w.sampler]),
};
}
this.ready = true;
}
capture(pass, layer, count) {
const index = DSPARK_LAYERS.indexOf(layer);
if (index >= 0)
this.dispatch(pass, "capture", this.captureGroups[index], H / 256, count);
}
updateContext(encoder, position, count) {
if (!count) return;
this.contextRun = position + count;
this.prepareStep(position, count);
const pass = encoder.beginComputePass();
pass.setBindGroup(0, this.stepGroup, [position * 256]);
const run = (name, g, x = 1, y = 1, z = 1) =>
this.dispatch(pass, name, g, x, y, z),
block = count <= 8;
run(
block ? "projectBlock" : "projectBatch",
block ? this.groups.projectBlock : this.groups.projectBatch,
H / (block ? 8 : 32),
);
run("norm", this.groups.hiddenNorm, count);
for (const { groups: g } of this.layers) {
run(
block ? "contextBlock" : "contextBatch",
block ? g.contextBlock : g.contextBatch,
(KV * 2) / (block ? 8 : 32),
);
run("contextNormRope", g.contextRope, 2, count);
run("store", g.store, KV / 32, count, 2);
}
pass.end();
}
propose(encoder, position) {
const from = Math.max(0, position - 64);
encoder.copyBufferToBuffer(
this.parent.work.tokens,
from * 4,
this.work.tokens,
from * 4,
(position - from + 1) * 4,
);
const count = 7,
plan = this.prepareStep(position, count),
pass = encoder.beginComputePass();
pass.setBindGroup(0, this.stepGroup, [position * 256]);
const run = (name, g, x = 1, y = 1, z = 1) =>
this.dispatch(pass, name, g, x, y, z);
run("noise", this.groups.noise);
this.parent.dispatch(pass, "embed", this.groups.embed, H / 256, count);
for (const { groups: g } of this.layers) {
run("norm", g.input, count);
run("qkvBlock", g.qkv, (H + KV * 2) / 8);
run("noiseNormRope", g.noiseRope, 18, count);
run("store", g.store, KV / 32, count, 2);
run("attention", g.attention, plan.chunks, 16, count);
run("combine", g.combine, 16, count);
run("outputBlock", g.output, H / 8);
run("norm", g.post, count);
run("upGateBlock", g.upGate, (F * 2) / 8);
run("silu", g.silu, F / 256, count);
run("downBlock", g.down, H / 8);
}
run("norm", this.groups.finalNorm, count);
this.parent.dispatch(pass, "headBlock", this.groups.head, VOCAB / 8);
for (let i = 0; i < 7; i++) {
this.device.queue.writeBuffer(
this.work.sampleSteps,
(position + i) * 256,
new Uint32Array([position + i, 1, 0, 1]),
);
pass.setBindGroup(0, this.sampleStepGroup, [(position + i) * 256]);
run("markovEmbed", this.groups.markovEmbed);
run("markov", this.groups.markov, VOCAB / 8);
run("addBias", this.biasGroups[i], VOCAB / 256);
if (this.parent.samplingSettings.temperature > 0) {
const keep = this.parent.sampleK <= 64 ? 64 : 256,
g = this.sortGroups[keep];
this.parent.encodeSample(pass, g.first, g);
this.parent.dispatch(pass, "saveCandidates", g.save[8 + i], keep / 64);
} else {
run("argmax", this.groups.argmax, 128);
run("argmaxFinal", this.groups.argmaxFinal);
}
}
pass.end();
encoder.copyBufferToBuffer(
this.work.tokens,
(position + 1) * 4,
this.parent.work.tokens,
(position + 1) * 4,
7 * 4,
);
}
}