The AI hub serves local models on the LAN and now yields GPU memory to the UI (shrinks the chat context, then unloads; grows back after; never loads into the UI reserve), groups models by type, and reports each model's memory split. LLM decode on Vulkan with Splash kernels, one shared GPU device per process, vision on chat turns, an on-device text detector, music models and stems. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
373 lines
13 KiB
Text
373 lines
13 KiB
Text
// Whisper: row-major f32 activations, packed f16 or f32 weights, f32 sums.
|
|
// Weight rows are padded to eight elements. Addresses and strides refer to
|
|
// elements, never descriptors. No kernel assumes a 32-lane subgroup.
|
|
let matrix = {
|
|
workgroup_size: 128
|
|
half_weights: 1
|
|
x: shader.buffer(f32)
|
|
x4: shader.buffer(vec4f)
|
|
w4: shader.buffer(vec4u)
|
|
wf4: shader.buffer(vec4f)
|
|
bias: shader.buffer(f32)
|
|
dst: shader.buffer_rw(f32)
|
|
m: shader.uniform(u32)
|
|
n: shader.uniform(u32)
|
|
k: shader.uniform(u32)
|
|
ws: shader.uniform(u32)
|
|
has_bias: shader.uniform(u32)
|
|
load_x: fn(row, col) {
|
|
if row < self.m && col + 3 < self.k && self.k % 4 == 0 {
|
|
return self.x4[(row * self.k + col) / 4]
|
|
}
|
|
var v = vec4f(0.0)
|
|
if row < self.m {
|
|
for c in 0..4 {
|
|
if col + c < self.k { v[c] = self.x[row * self.k + col + c] }
|
|
}
|
|
}
|
|
return v
|
|
}
|
|
}
|
|
|
|
// A 32x32 output tile and 32-wide reduction tile, 8 KiB shared memory.
|
|
// Each invocation accumulates two rows of four columns. The global reads
|
|
// are vec4f activations and vec4u packed weights (eight f16 values/load).
|
|
let gemm = matrix{
|
|
ax: shader.workgroup(f32, 1024)
|
|
bw: shader.workgroup(f32, 1024)
|
|
main: fn() {
|
|
let t = local_id().x
|
|
let row = t / 8
|
|
let col = (t % 8) * 4
|
|
let mr = group_id().y * 32
|
|
let nr = group_id().x * 32
|
|
var acc0 = vec4f(0.0)
|
|
var acc1 = vec4f(0.0)
|
|
var base = u32(0)
|
|
while base < self.k {
|
|
let a0 = self.load_x(mr + row, base + col)
|
|
let a1 = self.load_x(mr + row + 16, base + col)
|
|
for c in 0..4 {
|
|
self.ax[row * 32 + col + c] = a0[c]
|
|
self.ax[(row + 16) * 32 + col + c] = a1[c]
|
|
}
|
|
let wr = t / 4
|
|
let wc = (t % 4) * 8
|
|
var lo = vec4f(0.0)
|
|
var hi = vec4f(0.0)
|
|
if nr + wr < self.n && base + wc < self.k {
|
|
let off = (nr + wr) * self.ws + base + wc
|
|
if self.half_weights == 1 {
|
|
let p = self.w4[off / 8]
|
|
lo = vec4f(unpack_half2(p.x), unpack_half2(p.y))
|
|
hi = vec4f(unpack_half2(p.z), unpack_half2(p.w))
|
|
} else {
|
|
lo = self.wf4[off / 4]
|
|
hi = self.wf4[off / 4 + 1]
|
|
}
|
|
}
|
|
for c in 0..4 {
|
|
self.bw[(wc + c) * 32 + wr] = lo[c]
|
|
self.bw[(wc + c + 4) * 32 + wr] = hi[c]
|
|
}
|
|
barrier()
|
|
for j in 0..32 {
|
|
let w = vec4f(self.bw[j * 32 + col], self.bw[j * 32 + col + 1],
|
|
self.bw[j * 32 + col + 2], self.bw[j * 32 + col + 3])
|
|
acc0 += self.ax[row * 32 + j] * w
|
|
acc1 += self.ax[(row + 16) * 32 + j] * w
|
|
}
|
|
barrier()
|
|
base += 32
|
|
}
|
|
for c in 0..4 {
|
|
if nr + col + c < self.n {
|
|
var b = 0.0
|
|
if self.has_bias != 0 { b = self.bias[nr + col + c] }
|
|
if mr + row < self.m {
|
|
self.dst[(mr + row) * self.n + nr + col + c] = acc0[c] + b
|
|
}
|
|
if mr + row + 16 < self.m {
|
|
self.dst[(mr + row + 16) * self.n + nr + col + c] = acc1[c] + b
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Decode: four output rows per workgroup, eight weights per vector load.
|
|
// Reductions span all actual subgroups, including a single subgroup of 128.
|
|
let gemv = matrix{
|
|
partial: shader.workgroup(vec4f, 128)
|
|
main: fn() {
|
|
let t = local_id().x
|
|
let row = group_id().x * 4
|
|
let batch = group_id().y
|
|
var acc = vec4f(0.0)
|
|
var c = t * 8
|
|
while c < self.k {
|
|
let x0 = self.load_x(batch, c)
|
|
let x1 = self.load_x(batch, c + 4)
|
|
for r in 0..4 {
|
|
if row + r < self.n {
|
|
let off = (row + r) * self.ws + c
|
|
var lo = vec4f(0.0)
|
|
var hi = vec4f(0.0)
|
|
if self.half_weights == 1 {
|
|
let p = self.w4[off / 8]
|
|
lo = vec4f(unpack_half2(p.x), unpack_half2(p.y))
|
|
hi = vec4f(unpack_half2(p.z), unpack_half2(p.w))
|
|
} else {
|
|
lo = self.wf4[off / 4]
|
|
hi = self.wf4[off / 4 + 1]
|
|
}
|
|
acc[r] += dot(x0, lo) + dot(x1, hi)
|
|
}
|
|
}
|
|
c += self.workgroup_size * 8
|
|
}
|
|
let total = vec4f(subgroup_add(acc.x), subgroup_add(acc.y),
|
|
subgroup_add(acc.z), subgroup_add(acc.w))
|
|
if subgroup_lane() == 0 { self.partial[subgroup_id()] = total }
|
|
barrier()
|
|
if t == 0 {
|
|
var result = vec4f(0.0)
|
|
for s in 0..(self.workgroup_size + subgroup_size() - 1) / subgroup_size() {
|
|
result += self.partial[s]
|
|
}
|
|
for r in 0..4 {
|
|
if row + r < self.n {
|
|
var b = 0.0
|
|
if self.has_bias != 0 { b = self.bias[row + r] }
|
|
self.dst[batch * self.n + row + r] = result[r] + b
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
whisper_gemm_f16: gemm{}
|
|
whisper_gemm_f32: gemm{half_weights: 0}
|
|
whisper_gemv_f16: gemv{}
|
|
whisper_gemv_f32: gemv{half_weights: 0}
|
|
|
|
// op 0 add, 1 multiply, 2 GELU. b_len supports scalar, row and full
|
|
// elementwise broadcasts; callers validate the corresponding shapes.
|
|
whisper_elementwise: {
|
|
workgroup_size: 128
|
|
a: shader.buffer(f32)
|
|
b: shader.buffer(f32)
|
|
dst: shader.buffer_rw(f32)
|
|
n: shader.uniform(u32)
|
|
b_len: shader.uniform(u32)
|
|
op: shader.uniform(u32)
|
|
main: fn() {
|
|
let i = global_id().x
|
|
if i < self.n {
|
|
let x = self.a[i]
|
|
var out = x
|
|
if self.op == 0 {
|
|
out = x + self.b[i % self.b_len]
|
|
} else if self.op == 1 {
|
|
out = x * self.b[i % self.b_len]
|
|
} else {
|
|
out = 0.5 * x * (1.0 + tanh(0.7978845608 * (x + 0.044715 * x * x * x)))
|
|
}
|
|
self.dst[i] = out
|
|
}
|
|
}
|
|
}
|
|
|
|
// Two-pass variance matches the CPU formula (avoids cancellation in
|
|
// E[x*x] - E[x]*E[x]). Every lane participates in both reductions.
|
|
whisper_norm: {
|
|
workgroup_size: 128
|
|
x: shader.buffer(f32)
|
|
mul: shader.buffer(f32)
|
|
add: shader.buffer(f32)
|
|
dst: shader.buffer_rw(f32)
|
|
cols: shader.uniform(u32)
|
|
affine: shader.uniform(u32)
|
|
eps: shader.uniform(f32)
|
|
partial: shader.workgroup(f32, 128)
|
|
block_sum: fn(v) {
|
|
let s = subgroup_add(v)
|
|
if subgroup_lane() == 0 { self.partial[subgroup_id()] = s }
|
|
barrier()
|
|
var sum = 0.0
|
|
for j in 0..(self.workgroup_size + subgroup_size() - 1) / subgroup_size() {
|
|
sum += self.partial[j]
|
|
}
|
|
barrier()
|
|
return sum
|
|
}
|
|
main: fn() {
|
|
let t = local_id().x
|
|
let row = group_id().x * self.cols
|
|
var sum = 0.0
|
|
var c = t
|
|
while c < self.cols {
|
|
sum += self.x[row + c]
|
|
c += self.workgroup_size
|
|
}
|
|
let mean = self.block_sum(sum) / float(self.cols)
|
|
var variance = 0.0
|
|
c = t
|
|
while c < self.cols {
|
|
let delta = self.x[row + c] - mean
|
|
variance += delta * delta
|
|
c += self.workgroup_size
|
|
}
|
|
let inv = 1.0 / sqrt(self.block_sum(variance) / float(self.cols) + self.eps)
|
|
c = t
|
|
while c < self.cols {
|
|
var value = (self.x[row + c] - mean) * inv
|
|
if self.affine != 0 { value = value * self.mul[c] + self.add[c] }
|
|
self.dst[row + c] = value
|
|
c += self.workgroup_size
|
|
}
|
|
}
|
|
}
|
|
|
|
// Front-end convolution lowered to a GEMM. The first input is
|
|
// channels-first mel; subsequent convolution inputs are time-major.
|
|
whisper_im2col: {
|
|
workgroup_size: 128
|
|
x: shader.buffer(f32)
|
|
dst: shader.buffer_rw(f32)
|
|
ic: shader.uniform(u32)
|
|
iw: shader.uniform(u32)
|
|
ow: shader.uniform(u32)
|
|
kw: shader.uniform(u32)
|
|
stride: shader.uniform(u32)
|
|
pad: shader.uniform(u32)
|
|
time_major: shader.uniform(u32)
|
|
main: fn() {
|
|
let i = global_id().x
|
|
let width = self.ic * self.kw
|
|
if i < self.ow * width {
|
|
let t = i / width
|
|
let ci = (i % width) / self.kw
|
|
let kk = i % self.kw
|
|
let pos = i32(t * self.stride + kk) - i32(self.pad)
|
|
var v = 0.0
|
|
if pos >= 0 && pos < i32(self.iw) {
|
|
if self.time_major != 0 {
|
|
v = self.x[u32(pos) * self.ic + ci]
|
|
} else {
|
|
v = self.x[ci * self.iw + u32(pos)]
|
|
}
|
|
}
|
|
self.dst[i] = v
|
|
}
|
|
}
|
|
}
|
|
|
|
// Online attention: eight queries x 32 keys, 64 channels/head, <20 KiB
|
|
// shared memory. Q/K/V are read with 16-byte loads. Scores never leave
|
|
// the tile, so a 1500-frame encoder has no quadratic allocation.
|
|
// The 64-channel head is shared by every supported Whisper model.
|
|
whisper_attention: {
|
|
workgroup_size: 128
|
|
q: shader.buffer(vec4f)
|
|
k: shader.buffer(vec4f)
|
|
v: shader.buffer(vec4f)
|
|
dst: shader.buffer_rw(vec4f)
|
|
nq: shader.uniform(u32)
|
|
nk: shader.uniform(u32)
|
|
heads: shader.uniform(u32)
|
|
past: shader.uniform(u32)
|
|
causal: shader.uniform(u32)
|
|
scale: shader.uniform(f32)
|
|
qs: shader.workgroup(vec4f, 128)
|
|
ks: shader.workgroup(vec4f, 512)
|
|
vs: shader.workgroup(vec4f, 512)
|
|
scores: shader.workgroup(f32, 256)
|
|
maxima: shader.workgroup(f32, 8)
|
|
sums: shader.workgroup(f32, 8)
|
|
correction: shader.workgroup(f32, 8)
|
|
main: fn() {
|
|
let t = local_id().x
|
|
let head = group_id().x
|
|
let first = group_id().y * 8
|
|
let qi = t / 16
|
|
let dim = t % 16
|
|
let row_stride = self.heads * 16
|
|
var query = vec4f(0.0)
|
|
if first + qi < self.nq {
|
|
query = self.q[(first + qi) * row_stride + head * 16 + dim]
|
|
}
|
|
self.qs[t] = query
|
|
if t < 8 {
|
|
self.maxima[t] = -3.0e38
|
|
self.sums[t] = 0.0
|
|
}
|
|
barrier()
|
|
var acc = vec4f(0.0)
|
|
var base = u32(0)
|
|
while base < self.nk {
|
|
var p = t
|
|
while p < 512 {
|
|
let key = base + p / 16
|
|
let off = key * row_stride + head * 16 + p % 16
|
|
var key_vec = vec4f(0.0)
|
|
var val_vec = vec4f(0.0)
|
|
if key < self.nk {
|
|
key_vec = self.k[off]
|
|
val_vec = self.v[off]
|
|
}
|
|
self.ks[p] = key_vec
|
|
self.vs[p] = val_vec
|
|
p += self.workgroup_size
|
|
}
|
|
barrier()
|
|
// Each invocation owns two scores; no subgroup-size
|
|
// assumption or cross-subgroup shuffle is involved.
|
|
for pair in 0..2 {
|
|
let score_i = t + pair * 128
|
|
let qr = score_i / 32
|
|
let kr = score_i % 32
|
|
var score = -3.0e38
|
|
if first + qr < self.nq && base + kr < self.nk {
|
|
if self.causal == 0 || base + kr <= self.past + first + qr {
|
|
var product = 0.0
|
|
for d4 in 0..16 {
|
|
product += dot(self.qs[qr * 16 + d4], self.ks[kr * 16 + d4])
|
|
}
|
|
score = product * self.scale
|
|
}
|
|
}
|
|
self.scores[score_i] = score
|
|
}
|
|
barrier()
|
|
if dim == 0 {
|
|
var mx = self.maxima[qi]
|
|
for j in 0..32 { mx = max(mx, self.scores[qi * 32 + j]) }
|
|
let corr = exp(self.maxima[qi] - mx)
|
|
var sum = 0.0
|
|
for j in 0..32 {
|
|
let score = self.scores[qi * 32 + j]
|
|
var prob = 0.0
|
|
if score > -3.0e38 { prob = exp(score - mx) }
|
|
self.scores[qi * 32 + j] = prob
|
|
sum += prob
|
|
}
|
|
self.correction[qi] = corr
|
|
self.sums[qi] = self.sums[qi] * corr + sum
|
|
self.maxima[qi] = mx
|
|
}
|
|
barrier()
|
|
acc = acc * self.correction[qi]
|
|
for j in 0..32 {
|
|
acc += self.scores[qi * 32 + j] * self.vs[j * 16 + dim]
|
|
}
|
|
barrier()
|
|
base += 32
|
|
}
|
|
if first + qi < self.nq {
|
|
self.dst[(first + qi) * row_stride + head * 16 + dim] = acc / self.sums[qi]
|
|
}
|
|
}
|
|
}
|
|
}
|