enable f16;
struct MulMatIdGatherParams {
offset_ids: u32,
offset_dst: u32,
n_expert: u32,
n_expert_used: u32,
n_tokens: u32,
stride_ids_1: u32,
m: u32,
};
@group(0) @binding(0) var<storage, read_write> ids: array<i32>; // [n_expert_used, n_tokens]
@group(0) @binding(1) var<storage, read_write> ids_slot_pos: array<u32>; // [n_expert_used * n_tokens]
@group(0) @binding(2) var<storage, read_write> ids_tiles: array<u32>; // [n_tiles, (expert, first slot, n slots) * n_tiles]
@group(0) @binding(3) var<storage, read_write> dst: array<f32>; // [m, n_expert_used, n_tokens]
@group(0) @binding(4) var<uniform> params: MulMatIdGatherParams;
// per expert: the slot count, replaced by the first slot of the expert in ids_slot_pos by the prefix sum, then advanced as its write cursor
var<workgroup> ids_count: array<atomic<u32>, N_EXPERT>;
// launched as a single workgroup, where the threads process the elements of ids in parallel with atomics
@compute @workgroup_size(WG_SIZE)
fn main(@builtin(local_invocation_id) local_id: vec3<u32>) {
let thread_id = local_id.x;
let ids_slots_num = params.n_expert_used * params.n_tokens;
for (var e = thread_id; e < N_EXPERT; e += WG_SIZE) {
atomicStore(&ids_count[e], 0u);
}
workgroupBarrier();
// count the slots of each expert
for (var i = thread_id; i < ids_slots_num; i += WG_SIZE) {
let row = i / params.n_expert_used;
let col = i % params.n_expert_used;
let expert = ids[params.offset_ids + row * params.stride_ids_1 + col];
if (expert != -1) { // skipped slot, owned by no expert
atomicAdd(&ids_count[u32(expert)], 1u);
}
}
workgroupBarrier();
// exclusive prefix sum over the counts, and emit one tile descriptor per `TILE_N_SLOTS` slots of each expert
if (thread_id == 0u) {
var base: u32 = 0;
var tile: u32 = 0;
for (var e: u32 = 0; e < N_EXPERT; e += 1) {
let n = atomicLoad(&ids_count[e]);
atomicStore(&ids_count[e], base);
for (var k: u32 = 0; k < n; k += TILE_N_SLOTS) {
ids_tiles[1 + 3 * tile] = e;
ids_tiles[1 + 3 * tile + 1] = base + k;
ids_tiles[1 + 3 * tile + 2] = min(n - k, TILE_N_SLOTS);
tile += 1;
}
base += n;
}
ids_tiles[0] = tile;
}
workgroupBarrier();
// store the position in ids tensor to ids_slot_pos
for (var i = thread_id; i < ids_slots_num; i += WG_SIZE) {
let row = i / params.n_expert_used;
let col = i % params.n_expert_used;
let expert = ids[params.offset_ids + row * params.stride_ids_1 + col];
if (expert != -1) {
let pos = atomicAdd(&ids_count[u32(expert)], 1u);
ids_slot_pos[pos] = i;
}
}
// the matmul writes only the rows that an expert owns, so zero the rows of the skipped slots
for (var i: u32 = 0; i < ids_slots_num; i += 1) {
let row = i / params.n_expert_used;
let col = i % params.n_expert_used;
if (ids[params.offset_ids + row * params.stride_ids_1 + col] != -1) {
continue;
}
let dst_row = params.offset_dst + i * params.m;
for (var r = thread_id; r < params.m; r += WG_SIZE) {
dst[dst_row + r] = 0.0f;
}
}
}