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>
287 lines
11 KiB
Text
287 lines
11 KiB
Text
// Row-major dst[n,m] = W[n,k] * X[k,m]. W is f16. X is f32 or f16;
|
|
// f32 activations round to f16 while entering the shared tile. All matrix
|
|
// operations execute in every lane of a full subgroup. One, two or four
|
|
// subgroups cover the output tile, with four accumulators per subgroup.
|
|
let Coop = {
|
|
workgroup_size: 128
|
|
tm: 16
|
|
tn: 16
|
|
tk: 16
|
|
row_fragments: 4
|
|
col_fragments: 4
|
|
half_x: 0
|
|
vector_io: 1
|
|
half_vector8: 1
|
|
A: shader.coopmat(f16, 16, 16, shader.matrix_a)
|
|
B: shader.coopmat(f16, 16, 16, shader.matrix_b)
|
|
C: shader.coopmat(f32, 16, 16, shader.matrix_accumulator)
|
|
w: shader.buffer(f16)
|
|
w4: shader.buffer(vec4u)
|
|
x: shader.buffer(f32)
|
|
x4: shader.buffer(vec4f)
|
|
xh: shader.buffer(f16)
|
|
xh4: shader.buffer(vec4u)
|
|
xh2: shader.buffer(vec2u)
|
|
dst: shader.buffer_rw(f32)
|
|
n: shader.uniform(u32)
|
|
k: shader.uniform(u32)
|
|
m: shader.uniform(u32)
|
|
// 24 KiB total; the output scratch is used only for masked edge stores.
|
|
wt: shader.workgroup(f16, 2048)
|
|
xt: shader.workgroup(f16, 2048)
|
|
out: shader.workgroup(f32, 4096)
|
|
main: fn() {
|
|
let lane = local_id().x
|
|
let sg = subgroup_id()
|
|
let bn = u32(self.tm * self.row_fragments)
|
|
let bm = u32(self.tn * self.col_fragments)
|
|
let rn = group_id().x * bn
|
|
let cm = group_id().y * bm
|
|
let sr = (sg % 2) * self.tm * 2
|
|
let sc = (sg / 2) * self.tn * 2
|
|
var c00 = coopmat_fill(self.C, 0)
|
|
var c01 = coopmat_fill(self.C, 0)
|
|
var c10 = coopmat_fill(self.C, 0)
|
|
var c11 = coopmat_fill(self.C, 0)
|
|
var base = u32(0)
|
|
while base < self.k {
|
|
if self.vector_io == 1 {
|
|
var j = lane * 8
|
|
while j < bn * 32 {
|
|
let r = j / 32
|
|
let d = j % 32
|
|
var v = vec4u(0)
|
|
if rn + r < self.n && base + d < self.k {
|
|
v = self.w4[((rn + r) * self.k + base + d) / 8]
|
|
}
|
|
let p0 = unpack_half2(v.x)
|
|
let p1 = unpack_half2(v.y)
|
|
let p2 = unpack_half2(v.z)
|
|
let p3 = unpack_half2(v.w)
|
|
self.wt[j] = f16(p0.x)
|
|
self.wt[j+1] = f16(p0.y)
|
|
self.wt[j+2] = f16(p1.x)
|
|
self.wt[j+3] = f16(p1.y)
|
|
self.wt[j+4] = f16(p2.x)
|
|
self.wt[j+5] = f16(p2.y)
|
|
self.wt[j+6] = f16(p3.x)
|
|
self.wt[j+7] = f16(p3.y)
|
|
j += self.workgroup_size * 8
|
|
}
|
|
if self.half_x == 1 && self.half_vector8 == 1 {
|
|
j = lane * 8
|
|
while j < 32 * bm {
|
|
let d = j / bm
|
|
let c = j % bm
|
|
var v = vec4u(0)
|
|
if base + d < self.k && cm + c < self.m {
|
|
v = self.xh4[((base + d) * self.m + cm + c) / 8]
|
|
}
|
|
let p0 = unpack_half2(v.x)
|
|
let p1 = unpack_half2(v.y)
|
|
let p2 = unpack_half2(v.z)
|
|
let p3 = unpack_half2(v.w)
|
|
self.xt[j] = f16(p0.x)
|
|
self.xt[j+1] = f16(p0.y)
|
|
self.xt[j+2] = f16(p1.x)
|
|
self.xt[j+3] = f16(p1.y)
|
|
self.xt[j+4] = f16(p2.x)
|
|
self.xt[j+5] = f16(p2.y)
|
|
self.xt[j+6] = f16(p3.x)
|
|
self.xt[j+7] = f16(p3.y)
|
|
j += self.workgroup_size * 8
|
|
}
|
|
} else if self.half_x == 1 {
|
|
// Four f16 values keep rows with an 8-byte stride
|
|
// aligned, while weights still use 16-byte reads.
|
|
j = lane * 4
|
|
while j < 32 * bm {
|
|
let d = j / bm
|
|
let c = j % bm
|
|
var v = vec2u(0)
|
|
if base + d < self.k && cm + c < self.m {
|
|
v = self.xh2[((base + d) * self.m + cm + c) / 4]
|
|
}
|
|
let p0 = unpack_half2(v.x)
|
|
let p1 = unpack_half2(v.y)
|
|
self.xt[j] = f16(p0.x)
|
|
self.xt[j+1] = f16(p0.y)
|
|
self.xt[j+2] = f16(p1.x)
|
|
self.xt[j+3] = f16(p1.y)
|
|
j += self.workgroup_size * 4
|
|
}
|
|
} else {
|
|
j = lane * 4
|
|
while j < 32 * bm {
|
|
let d = j / bm
|
|
let c = j % bm
|
|
var v = vec4f(0)
|
|
if base + d < self.k && cm + c < self.m {
|
|
v = self.x4[((base + d) * self.m + cm + c) / 4]
|
|
}
|
|
self.xt[j] = f16(v.x)
|
|
self.xt[j+1] = f16(v.y)
|
|
self.xt[j+2] = f16(v.z)
|
|
self.xt[j+3] = f16(v.w)
|
|
j += self.workgroup_size * 4
|
|
}
|
|
}
|
|
} else {
|
|
var j = lane
|
|
while j < bn * 32 {
|
|
let r = j / 32
|
|
let d = j % 32
|
|
var v = f16(0)
|
|
if rn + r < self.n && base + d < self.k {
|
|
v = self.w[(rn + r) * self.k + base + d]
|
|
}
|
|
self.wt[j] = v
|
|
j += self.workgroup_size
|
|
}
|
|
j = lane
|
|
while j < 32 * bm {
|
|
let d = j / bm
|
|
let c = j % bm
|
|
var v = f16(0)
|
|
if base + d < self.k && cm + c < self.m {
|
|
if self.half_x == 1 { v = self.xh[(base + d) * self.m + cm + c] }
|
|
else { v = f16(self.x[(base + d) * self.m + cm + c]) }
|
|
}
|
|
self.xt[j] = v
|
|
j += self.workgroup_size
|
|
}
|
|
}
|
|
barrier()
|
|
for d in 0..32 / self.tk {
|
|
let a0 = coopmat_load(self.A, self.wt[sr * 32 + d * self.tk], 32, 0)
|
|
let a1 = coopmat_load(self.A, self.wt[(sr + self.tm) * 32 + d * self.tk], 32, 0)
|
|
let b0 = coopmat_load(self.B, self.xt[d * self.tk * bm + sc], bm, 0)
|
|
let b1 = coopmat_load(self.B, self.xt[d * self.tk * bm + sc + self.tn], bm, 0)
|
|
c00 = coopmat_mul_add(a0, b0, c00)
|
|
c01 = coopmat_mul_add(a0, b1, c01)
|
|
c10 = coopmat_mul_add(a1, b0, c10)
|
|
c11 = coopmat_mul_add(a1, b1, c11)
|
|
}
|
|
barrier()
|
|
base += 32
|
|
}
|
|
if rn + bn <= self.n && cm + bm <= self.m {
|
|
coopmat_store(c00, self.dst[(rn + sr) * self.m + cm + sc], self.m, 0)
|
|
coopmat_store(c01, self.dst[(rn + sr) * self.m + cm + sc + self.tn], self.m, 0)
|
|
coopmat_store(c10, self.dst[(rn + sr + self.tm) * self.m + cm + sc], self.m, 0)
|
|
coopmat_store(c11, self.dst[(rn + sr + self.tm) * self.m + cm + sc + self.tn], self.m, 0)
|
|
} else {
|
|
coopmat_store(c00, self.out[sr * bm + sc], bm, 0)
|
|
coopmat_store(c01, self.out[sr * bm + sc + self.tn], bm, 0)
|
|
coopmat_store(c10, self.out[(sr + self.tm) * bm + sc], bm, 0)
|
|
coopmat_store(c11, self.out[(sr + self.tm) * bm + sc + self.tn], bm, 0)
|
|
barrier()
|
|
var j = lane
|
|
while j < bn * bm {
|
|
let r = j / bm
|
|
let c = j % bm
|
|
if rn + r < self.n && cm + c < self.m {
|
|
self.dst[(rn + r) * self.m + cm + c] = self.out[j]
|
|
}
|
|
j += self.workgroup_size
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Shared-tile fallback. Each lane accumulates a 2x2 register tile. It uses
|
|
// the same f16 input rounding as the cooperative variants.
|
|
let Shared = {
|
|
workgroup_size: 256
|
|
half_x: 0
|
|
w: shader.buffer(f16)
|
|
x: shader.buffer(f32)
|
|
xh: shader.buffer(f16)
|
|
dst: shader.buffer_rw(f32)
|
|
n: shader.uniform(u32)
|
|
k: shader.uniform(u32)
|
|
m: shader.uniform(u32)
|
|
wt: shader.workgroup(f32, 1056)
|
|
xt: shader.workgroup(f32, 1056)
|
|
main: fn() {
|
|
let lane = local_id().x
|
|
let columns = u32(self.workgroup_size / 16)
|
|
let r = lane / columns
|
|
let c = lane % columns
|
|
let rn = group_id().x * 32
|
|
let cm = group_id().y * columns * 2
|
|
var c00 = 0.0
|
|
var c01 = 0.0
|
|
var c10 = 0.0
|
|
var c11 = 0.0
|
|
var base = u32(0)
|
|
while base < self.k {
|
|
var j = lane
|
|
while j < 1024 {
|
|
let rr = j / 32
|
|
let cc = j % 32
|
|
var a = 0.0
|
|
var b = 0.0
|
|
if rn + rr < self.n && base + cc < self.k {
|
|
a = f32(self.w[(rn + rr) * self.k + base + cc])
|
|
}
|
|
if base + rr < self.k && cc < columns * 2 && cm + cc < self.m {
|
|
if self.half_x == 1 { b = f32(self.xh[(base + rr) * self.m + cm + cc]) }
|
|
else { b = f32(f16(self.x[(base + rr) * self.m + cm + cc])) }
|
|
}
|
|
self.wt[cc * 33 + rr] = a
|
|
self.xt[rr * 33 + cc] = b
|
|
j += self.workgroup_size
|
|
}
|
|
barrier()
|
|
for d in 0..32 {
|
|
let a0 = self.wt[d * 33 + r]
|
|
let a1 = self.wt[d * 33 + r + 16]
|
|
let b0 = self.xt[d * 33 + c]
|
|
let b1 = self.xt[d * 33 + c + columns]
|
|
c00 += a0 * b0
|
|
c01 += a0 * b1
|
|
c10 += a1 * b0
|
|
c11 += a1 * b1
|
|
}
|
|
barrier()
|
|
base += 32
|
|
}
|
|
if rn + r < self.n {
|
|
if cm + c < self.m { self.dst[(rn + r) * self.m + cm + c] = c00 }
|
|
if cm + c + columns < self.m { self.dst[(rn + r) * self.m + cm + c + columns] = c01 }
|
|
}
|
|
if rn + r + 16 < self.n {
|
|
if cm + c < self.m { self.dst[(rn + r + 16) * self.m + cm + c] = c10 }
|
|
if cm + c + columns < self.m { self.dst[(rn + r + 16) * self.m + cm + c + columns] = c11 }
|
|
}
|
|
}
|
|
}
|
|
{
|
|
gemm_coop_16x16x16: Coop{}
|
|
gemm_coop_16x16x32: Coop{
|
|
tk: 32
|
|
A: shader.coopmat(f16, 16, 32, shader.matrix_a)
|
|
B: shader.coopmat(f16, 32, 16, shader.matrix_b)
|
|
}
|
|
gemm_coop_16x8x16: Coop{
|
|
tn: 8
|
|
B: shader.coopmat(f16, 16, 8, shader.matrix_b)
|
|
C: shader.coopmat(f32, 16, 8, shader.matrix_accumulator)
|
|
}
|
|
gemm_coop_16x8x8: Coop{
|
|
tn: 8
|
|
tk: 8
|
|
A: shader.coopmat(f16, 16, 8, shader.matrix_a)
|
|
B: shader.coopmat(f16, 8, 8, shader.matrix_b)
|
|
C: shader.coopmat(f32, 16, 8, shader.matrix_accumulator)
|
|
}
|
|
gemm_coop_8x8x16: Coop{
|
|
tm: 8
|
|
tn: 8
|
|
A: shader.coopmat(f16, 8, 16, shader.matrix_a)
|
|
B: shader.coopmat(f16, 16, 8, shader.matrix_b)
|
|
C: shader.coopmat(f32, 8, 8, shader.matrix_accumulator)
|
|
}
|
|
gemm_shared: Shared{}
|
|
}
|