makepad/libs/ai/vulkan/kernels/gemm_int8.splash
Admin 74faf62ef1 ai: the AI hub gives way to the UI, smaller models, vision and speech
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>
2026-10-13 12:26:21 +02:00

323 lines
12 KiB
Text

// Symmetric int8, one scale per weight row and activation column. Packed
// operands are row/column major with K padded to 64. Metadata is (scale,
// sum of signed codes) per 32768-value span, keeping raw dot sums in i32.
let Quantize = {
workgroup_size: 128
vector_x: 1
transpose_io: 0
x: shader.buffer(f32)
x4: shader.buffer(vec4f)
xq: shader.buffer_rw(u32)
xs: shader.buffer_rw(vec2f)
k: shader.uniform(u32)
kp: shader.uniform(u32)
m: shader.uniform(u32)
partial: shader.workgroup(vec4f, 128)
read: fn(d, c) {
var v = vec4f(0)
if d < self.k {
if self.transpose_io == 1 {
for t in 0..4 {
if c + t < self.m { v[t] = self.x[(c + t) * self.k + d] }
}
} else if self.vector_x == 1 { v = self.x4[(d * self.m + c) / 4] }
else {
for t in 0..4 {
if c + t < self.m { v[t] = self.x[d * self.m + c + t] }
}
}
}
return v
}
quant: fn(v, inv) {
let z = v * inv
return clamp(sign(z) * floor(abs(z) + 0.5), vec4f(-127), vec4f(127))
}
main: fn() {
let lane = local_id().x
let col = group_id().x * 4
var amax = vec4f(0)
let blocks = self.k / self.workgroup_size + min(self.k % self.workgroup_size, u32(1))
for block in 0..blocks {
let d = block * self.workgroup_size + lane
amax = max(amax, abs(self.read(d, col)))
}
self.partial[lane] = amax
barrier()
var stride = u32(self.workgroup_size / 2)
while stride > 0 {
if lane < stride {
self.partial[lane] = max(self.partial[lane], self.partial[lane + stride])
}
barrier()
stride /= 2
}
let scale = self.partial[0] / 127.0
var inv = vec4f(0)
for t in 0..4 { if scale[t] > 0.0 { inv[t] = 1.0 / scale[t] } }
barrier()
var start = u32(0)
// One empty span still writes zero scales and sums for K=0.
while start < max(self.kp, u32(1)) {
let end = start + min(u32(32768), self.kp - start)
var sum = vec4f(0)
let width = u32(self.workgroup_size * 4)
let chunks = (end - start) / width + min((end - start) % width, u32(1))
for block in 0..chunks {
let d = start + block * width + lane * 4
if d < end {
let a = self.quant(self.read(d, col), inv)
let b = self.quant(self.read(d + 1, col), inv)
let c = self.quant(self.read(d + 2, col), inv)
let e = self.quant(self.read(d + 3, col), inv)
sum += a + b + c + e
for t in 0..4 {
if col + t < self.m {
let word = (u32(i32(a[t])) & 255) | ((u32(i32(b[t])) & 255) << 8)
| ((u32(i32(c[t])) & 255) << 16) | ((u32(i32(e[t])) & 255) << 24)
self.xq[(col + t) * (self.kp / 4) + d / 4] = word
}
}
}
}
self.partial[lane] = sum
barrier()
stride = u32(self.workgroup_size / 2)
while stride > 0 {
if lane < stride { self.partial[lane] += self.partial[lane + stride] }
barrier()
stride /= 2
}
if lane == 0 {
let total = self.partial[0]
for t in 0..4 {
if col + t < self.m {
self.xs[(start / 32768) * self.m + col + t] = vec2f(scale[t], total[t])
}
}
}
barrier()
if end == self.kp { break }
start = end
}
}
}
let Packed = {
transpose_io: 0
w4: shader.buffer(vec4u)
x4: shader.buffer(vec4u)
ws: shader.buffer(vec2f)
xs: shader.buffer(vec2f)
dst: shader.buffer_rw(f32)
bias: shader.buffer(f32)
has_bias: shader.uniform(u32)
n: shader.uniform(u32)
kp: shader.uniform(u32)
m: shader.uniform(u32)
start: shader.uniform(u32)
end: shader.uniform(u32)
output: fn(r, c, v) {
let wm = self.ws[(self.start / 32768) * self.n + r]
let xm = self.xs[(self.start / 32768) * self.m + c]
var value = f32(v) * wm.x * xm.x
var at = r * self.m + c
if self.transpose_io == 1 { at = c * self.n + r }
if self.start > 0 { value += self.dst[at] }
if self.transpose_io == 1 && self.has_bias != 0 && self.end == self.kp { value += self.bias[r] }
self.dst[at] = value
}
}
// 64x64, K=64; every global and shared operand load is 16 bytes.
// Offset correction happens once per output, outside the dot-product loop.
let Dot = Packed{
workgroup_size: 256
rows: 4
wt: shader.workgroup(vec4u, 256)
xt: shader.workgroup(vec4u, 256)
dot16: fn(a, b) {
return i32(dot4_u8(a.x, b.x) + dot4_u8(a.y, b.y)
+ dot4_u8(a.z, b.z) + dot4_u8(a.w, b.w))
}
dots: fn(a, b0, b1, b2, b3) {
return vec4i(self.dot16(a, b0), self.dot16(a, b1), self.dot16(a, b2), self.dot16(a, b3))
}
store: fn(r, c, v) {
if r < self.n {
let wm = self.ws[(self.start / 32768) * self.n + r]
for t in 0..4 {
if c + t < self.m {
let xm = self.xs[(self.start / 32768) * self.m + c + t]
let correction = 128 * (i32(wm.y) + i32(xm.y)) + i32(self.end - self.start) * 16384
self.output(r, c + t, v[t] - correction)
}
}
}
}
main: fn() {
let lane = local_id().x
let rn = group_id().x * 64
let cm = group_id().y * 64
let r = (lane / 16) * self.rows
let c = (lane % 16) * 4
var c0 = vec4i(0)
var c1 = vec4i(0)
var c2 = vec4i(0)
var c3 = vec4i(0)
var c4 = vec4i(0)
var c5 = vec4i(0)
var c6 = vec4i(0)
var c7 = vec4i(0)
var base = self.start
while base < self.end {
var j = lane
while j < 256 {
let row = j / 4
let d = (j % 4) * 16
var a = vec4u(0)
var b = vec4u(0)
if rn + row < self.n { a = self.w4[((rn + row) * self.kp + base + d) / 16] }
if cm + row < self.m { b = self.x4[((cm + row) * self.kp + base + d) / 16] }
let flip = vec4u(u32(2155905152))
self.wt[j] = a ^ flip
self.xt[j] = b ^ flip
j += self.workgroup_size
}
barrier()
for d in 0..4 {
let b0 = self.xt[c * 4 + d]
let b1 = self.xt[(c + 1) * 4 + d]
let b2 = self.xt[(c + 2) * 4 + d]
let b3 = self.xt[(c + 3) * 4 + d]
c0 += self.dots(self.wt[r * 4 + d], b0, b1, b2, b3)
c1 += self.dots(self.wt[(r + 1) * 4 + d], b0, b1, b2, b3)
c2 += self.dots(self.wt[(r + 2) * 4 + d], b0, b1, b2, b3)
c3 += self.dots(self.wt[(r + 3) * 4 + d], b0, b1, b2, b3)
if self.rows == 8 {
c4 += self.dots(self.wt[(r + 4) * 4 + d], b0, b1, b2, b3)
c5 += self.dots(self.wt[(r + 5) * 4 + d], b0, b1, b2, b3)
c6 += self.dots(self.wt[(r + 6) * 4 + d], b0, b1, b2, b3)
c7 += self.dots(self.wt[(r + 7) * 4 + d], b0, b1, b2, b3)
}
}
barrier()
base += 64
}
self.store(rn + r, cm + c, c0)
self.store(rn + r + 1, cm + c, c1)
self.store(rn + r + 2, cm + c, c2)
self.store(rn + r + 3, cm + c, c3)
if self.rows == 8 {
self.store(rn + r + 4, cm + c, c4)
self.store(rn + r + 5, cm + c, c5)
self.store(rn + r + 6, cm + c, c6)
self.store(rn + r + 7, cm + c, c7)
}
}
}
// Signed cooperative matrices read the same packed bytes as Dot. The
// 16-row shape uses four column accumulators per subgroup; the 64-row
// shape uses one. The largest workgroup tile needs 20 KiB shared memory.
let Coop = Packed{
workgroup_size: 128
subgroups: 4
wide: 0
tk: 16
A: shader.coopmat(shader.i8, 16, 16, shader.matrix_a)
B: shader.coopmat(shader.i8, 16, 16, shader.matrix_b)
C: shader.coopmat(i32, 16, 16, shader.matrix_accumulator)
wt: shader.workgroup(vec4u, 128)
xt: shader.workgroup(vec4u, 128)
out: shader.workgroup(i32, 4096)
main: fn() {
let lane = local_id().x
let sg = subgroup_id()
var bn = u32(16 * self.subgroups)
var bm = u32(64)
var sr = sg * 16
var sc = u32(0)
if self.wide == 1 {
bn = u32(64)
bm = u32(16 * self.subgroups)
sr = u32(0)
sc = sg * 16
}
let rn = group_id().x * bn
let cm = group_id().y * bm
var c0 = coopmat_fill(self.C, 0)
var c1 = coopmat_fill(self.C, 0)
var c2 = coopmat_fill(self.C, 0)
var c3 = coopmat_fill(self.C, 0)
var base = self.start
while base < self.end {
var j = lane
while j < bn * 2 {
let r = j / 2
let d = (j % 2) * 16
var v = vec4u(0)
if rn + r < self.n { v = self.w4[((rn + r) * self.kp + base + d) / 16] }
self.wt[j] = v
j += self.workgroup_size
}
j = lane
while j < bm * 2 {
let c = j / 2
let d = (j % 2) * 16
var v = vec4u(0)
if cm + c < self.m { v = self.x4[((cm + c) * self.kp + base + d) / 16] }
self.xt[j] = v
j += self.workgroup_size
}
barrier()
for d in 0..32 / self.tk {
let a = coopmat_load(self.A, self.wt[sr * 2 + d * (self.tk / 16)], 2, 0)
let b0 = coopmat_load(self.B, self.xt[sc * 2 + d * (self.tk / 16)], 2, 1)
c0 = coopmat_mul_add(a, b0, c0)
if self.wide == 0 {
let b1 = coopmat_load(self.B, self.xt[32 + d * (self.tk / 16)], 2, 1)
let b2 = coopmat_load(self.B, self.xt[64 + d * (self.tk / 16)], 2, 1)
let b3 = coopmat_load(self.B, self.xt[96 + d * (self.tk / 16)], 2, 1)
c1 = coopmat_mul_add(a, b1, c1)
c2 = coopmat_mul_add(a, b2, c2)
c3 = coopmat_mul_add(a, b3, c3)
}
}
barrier()
base += 32
}
coopmat_store(c0, self.out[sr * bm + sc], bm, 0)
if self.wide == 0 {
coopmat_store(c1, self.out[sr * bm + 16], bm, 0)
coopmat_store(c2, self.out[sr * bm + 32], bm, 0)
coopmat_store(c3, self.out[sr * bm + 48], 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.output(rn + r, cm + c, self.out[j]) }
j += self.workgroup_size
}
}
}
{
gemm_quantize_columns: Quantize{}
gemm_quantize_columns_nt: Quantize{transpose_io: 1}
gemm_i8_dot4_4x4: Dot{}
gemm_i8_dot4_nt_4x4: Dot{transpose_io: 1}
gemm_i8_dot4_8x4: Dot{rows: 8 workgroup_size: 128}
gemm_i8_coop_16x16x16: Coop{}
gemm_i8_coop_64x16x16: Coop{
wide: 1
A: shader.coopmat(shader.i8, 64, 16, shader.matrix_a)
C: shader.coopmat(i32, 64, 16, shader.matrix_accumulator)
}
gemm_i8_coop_16x16x32: Coop{
tk: 32
A: shader.coopmat(shader.i8, 16, 32, shader.matrix_a)
B: shader.coopmat(shader.i8, 32, 16, shader.matrix_b)
}
}