9f30916003
Re-derives the campaign's rtop8 breakthrough (fuse/rtop8-par @ b32439b, commits 48b3a98 + b32439b) cleanly onto origin/dev @61004dc(post-#391 split), dropping the cb-chain commit (1130ac9) that branch was stacked on. cb-chain touched only c/glm.c (now split into c/colibri.c); rtop8 touches only c/backend_metal.{h,mm} and c/tests/test_backend_metal.mm — disjoint files, verified zero cb-chain remnants in this diff. - r_top8_par: one SIMDGROUP per row (blocked lane ownership, taken bitmask, shuffle-down argmax with ties->lower-index) replicating the serial r_top8's first-max-wins ascending order exactly; topp/normk/ rscale tail verbatim on lane 0. Bench: serial 0.465 ms/layer (~55% of the layer CB), parallel ~93x faster on the kernel, bit-exact. - COLI_RTOP8 gate, default ON (renamed and inverted from the campaign's COLI_RTOP8_PAR=0-default gate; COLI_RTOP8=0 is the opt-out escape). - Expert-count generality (new hard requirement, REAP E=168 packages): the parallel kernel's E<=256 contract is now enforced at EVERY call site in host code (g_rtop8_width_ok / E<=256 checked before selecting the pipeline), not just as an in-kernel defensive no-op -- closes a latent gap where the campaign's SIMD-width guard only protected the engine's automatic dispatch, not the standalone coli_metal_rtop8(par=1, ...) probe function metal-test itself uses. Out-of-contract requests (E>256, or non-32-wide SIMD) now transparently run the serial kernel instead of silently no-op'ing. - metal-test: 16 new cases -- the campaign's 10-case exact-match fuzz (now E-parametric) plus 6 new E-generality cases: E=168 (REAP) generic + massed-dup-ties, E=24 (<32 lane width) generic + ALL-EQUAL ties, E=200 (a lane straddles the E boundary -- per=ceil(200/32)=7 doesn't divide 200, so lane 28's ch[] block mixes 4 real indices with 3 sentinel ones; input is constructed to force those 4 into the top-8 deterministically, and the test asserts they were actually selected, not just that serial==parallel), and E=257 (out-of-contract, proves the auto-serial-fallback engages: this case is confirmed load-bearing -- it fails without the new host-side guard). 15 stock + 10 ported + 6 new = 31 total metal-test cases. Builds (METAL=0/1) and full C/python suites verified unchanged vs stock61004dc(0 new warnings, identical pass counts). On-box A/B on this branch is pending (see PR_BODY.md) -- no model runs performed here. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
161 lines
8.5 KiB
C
161 lines
8.5 KiB
C
#ifndef COLIBRI_BACKEND_METAL_H
|
|
#define COLIBRI_BACKEND_METAL_H
|
|
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
|
|
#ifdef __cplusplus
|
|
extern "C" {
|
|
#endif
|
|
|
|
/*
|
|
* Apple-GPU (Metal) backend for colibrì. Apple Silicon has one GPU and unified
|
|
* memory, so there is no device list and no host<->device copy: resident weights
|
|
* are read zero-copy from the RAM they already occupy. The shader is compiled at
|
|
* runtime (newLibraryWithSource:), so no Xcode / offline metal compiler is needed.
|
|
*/
|
|
|
|
/* Opaque, persistent GPU handle for one resident quantized tensor. */
|
|
typedef struct ColiMetalTensor ColiMetalTensor;
|
|
|
|
/* Returns 1 if a Metal device is available and pipelines compiled, else 0. */
|
|
int coli_metal_init(void);
|
|
void coli_metal_shutdown(void);
|
|
int coli_metal_available(void);
|
|
/* Bytes of unified memory in use by wrapped tensors, and their count. */
|
|
void coli_metal_stats(size_t *tensor_count, size_t *tensor_bytes);
|
|
int coli_metal_mem_info(size_t *used_bytes, size_t *total_bytes);
|
|
|
|
/*
|
|
* y[S,O] = (x[S,I] @ W[O,I]^T) * scale[o].
|
|
* fmt matches QT in glm.c: 0=f32, 1=int8, 2=int4(packed), 3=int2(packed).
|
|
* The first successful call wraps W and its row scales in GPU-visible buffers;
|
|
* later calls reuse them (weights are assumed stable at the same address).
|
|
* Returns 1 on success, 0 if Metal is unavailable or fmt is invalid.
|
|
*/
|
|
int coli_metal_matmul(ColiMetalTensor **tensor,
|
|
float *y, const float *x,
|
|
const void *weights, const float *scales,
|
|
int fmt, int S, int I, int O);
|
|
|
|
void coli_metal_tensor_free(ColiMetalTensor *tensor);
|
|
size_t coli_metal_tensor_bytes(const ColiMetalTensor *tensor);
|
|
|
|
/*
|
|
* Register a page-aligned host allocation (expert slab / scale slab) so the batched
|
|
* MoE path can read it zero-copy: the backend wraps it once in an MTLBuffer
|
|
* (newBufferWithBytesNoCopy) and resolves any pointer inside [base,base+len) to a GPU
|
|
* address. Call after (re)allocating a slab; call unregister before freeing it.
|
|
* base must be aligned to 16384 (Apple page) and len a multiple of it.
|
|
*/
|
|
void coli_metal_spin_start(void); /* COLI_METAL_SPIN=1 keep-alive experiment */
|
|
void coli_metal_spin_stop(void);
|
|
void coli_metal_register(void *base, size_t len);
|
|
void coli_metal_unregister(void *base);
|
|
|
|
/*
|
|
* Fused decode (S=1) attention for one layer, entirely on the GPU in one command buffer:
|
|
* q_a -> rmsnorm -> q_b -> RoPE ; kv_a -> latent rmsnorm@pos + krot RoPE@pos (cache write) ;
|
|
* MLA absorption core ; o_proj. Weights (q_a/q_b/kv_a/kv_b/o) and the Lc/Rc caches must be
|
|
* registered (page-aligned) for zero-copy resolve. GLM-5.2 dims compiled in. Handles st0==0
|
|
* full-range only. Returns 1 on success, 0 to signal CPU fallback.
|
|
*/
|
|
/*
|
|
* Full decode layer in ONE command buffer: in_ln -> attention -> residual -> post_ln ->
|
|
* shared expert -> router+top-K (exact phase-A semantics). x updated in place; nrm_out
|
|
* is the expert input; sh_out the shared-expert output; idx/w/keff the routing.
|
|
* Returns 0 -> caller runs the whole layer on the CPU path.
|
|
*/
|
|
int coli_metal_layer_decode(float *x,
|
|
const float *in_ln, const float *post_ln,
|
|
const void *qa_w, const float *qa_s, int qa_fmt, const float *qa_ln,
|
|
const void *qb_w, const float *qb_s, int qb_fmt,
|
|
const void *kva_w, const float *kva_s, int kva_fmt, const float *kva_ln,
|
|
const void *kvb_w, const float *kvb_s, int kvb_fmt,
|
|
const void *o_w, const float *o_s, int o_fmt,
|
|
const void *shg_w, const float *shg_s, int shg_fmt,
|
|
const void *shu_w, const float *shu_s, int shu_fmt,
|
|
const void *shd_w, const float *shd_s, int shd_fmt,
|
|
const float *router_w, const float *router_bias,
|
|
int E, int K, int Ksel, float topp, int normk, float rscale,
|
|
float *Lc, float *Rc, int S, int pos_base, int st0,
|
|
float eps, float theta, float ascale,
|
|
float *inrm_out, float *nrm_out, float *sh_out, int *idx_out, float *w_out, int *keff_out);
|
|
|
|
int coli_metal_gemm(float *y, const float *x, const void *weights, const float *scales,
|
|
int fmt, int S, int I, int O); /* large-batch sync GEMM; 0 -> CPU */
|
|
/* Parallel top-8 expert selection (r_top8_par): run ONE top-8 selection kernel standalone
|
|
* on host arrays — par=0 the serial r_top8, par=1 the parallel exact-match replica gated
|
|
* in the engine by COLI_RTOP8 (default ON; COLI_RTOP8=0 opts out to the serial kernel).
|
|
* Exists so the metal-test suite (and any battery probe) can prove serial/parallel
|
|
* equivalence on the ENGINE build's own compiled shaders, not just in the bench tool.
|
|
* sig[S*E], bias[E], idx[S*K], w[S*K], keff[S].
|
|
* Expert-count generality: the parallel kernel's blocked-lane design (ch[8]/32-lane
|
|
* threadgroup) is validated correct for arbitrary E<=256, including non-multiples of the
|
|
* 32-lane width and small E (see metal-test's E=24/E=168/E=256 cases — 168 is the REAP
|
|
* expert-pruned package width from #428/#426). For E>256 (out of contract) this function
|
|
* transparently falls back to the serial kernel even when par=1 is requested, and the
|
|
* same automatic fallback is wired into the engine dispatch site — "par" is a request,
|
|
* never a guarantee, so no caller can reach the unguarded parallel path out of contract.
|
|
* Returns 1 on success, 0 if Metal is unavailable. */
|
|
int coli_metal_rtop8(int par, const float *sig, const float *bias, int S, int E, int K,
|
|
int Ksel, float topp, int normk, float rscale,
|
|
int *idx, float *w, int *keff);
|
|
void coli_metal_attn_counts(uint64_t *ok, double *wall, double *kernel);
|
|
void coli_metal_attn_lat(double *ksched, double *gsched);
|
|
int coli_metal_attn_decode(const float *x,
|
|
const void *qa_w, const float *qa_s, int qa_fmt, const float *qa_ln,
|
|
const void *qb_w, const float *qb_s, int qb_fmt,
|
|
const void *kva_w, const float *kva_s, int kva_fmt, const float *kva_ln,
|
|
const void *kvb_w, const float *kvb_s, int kvb_fmt,
|
|
const void *o_w, const float *o_s, int o_fmt,
|
|
float *Lc, float *Rc, int S, int pos_base, int st0, float eps, float theta, float ascale, float *out);
|
|
|
|
/* Diagnostics: GPU blocks executed, CPU-fallback blocks, experts run on GPU. */
|
|
void coli_metal_moe_counts(uint64_t *ok, uint64_t *fb, uint64_t *experts);
|
|
void coli_metal_moe_times(double *setup, double *gpu, double *scatter);
|
|
double coli_metal_moe_kernel_time(void);
|
|
|
|
/*
|
|
* Batched routed-expert SwiGLU for one MoE block, in ONE command buffer.
|
|
* For each expert e in [0,nb): computes hh_e[nr_e, D] = down( silu(gate(xg_e)) * up(xg_e) )
|
|
* and scatter-adds rw * hh_e into out. All experts share the command buffer so the
|
|
* ~150us Metal launch latency is paid once per block, not per matmul.
|
|
*
|
|
* D = hidden size, Iinter = moe intermediate size
|
|
* g/u/d[e] = pointers to expert e's gate/up/down quantized weights (in RAM slabs)
|
|
* gs/us/ds[e] = pointers to expert e's per-row scales
|
|
* fmt = quant format (shared across experts)
|
|
* xg = packed activations [total_rows, D]; xoff[e] = row offset of expert e
|
|
* nr[e] = rows for expert e; rows[]/rw[] map packed rows back to out positions
|
|
* out = [S, D] accumulate target
|
|
* Returns 1 on success, 0 to signal the caller to fall back to the CPU path.
|
|
*/
|
|
int coli_metal_moe_block(int nb, int D, int Iinter, int fmt,
|
|
const void *const *g, const void *const *u, const void *const *d,
|
|
const float *const *gs, const float *const *us, const float *const *ds,
|
|
const float *xg, const int *xoff, const int *nr,
|
|
const int *rows, const float *rw,
|
|
float *out, int S);
|
|
|
|
/*
|
|
* Async two-phase variant: begin encodes+commits the block (own scratch, no wait) and
|
|
* returns a handle, so the CPU can load missed experts from disk WHILE the GPU computes
|
|
* the resident ones; end waits, checks for GPU faults, scatter-adds into out, and frees
|
|
* the handle. begin returns NULL (nothing submitted) on unresolved slab / bad fmt / R==0;
|
|
* end returns 0 on GPU fault (caller redoes those experts on CPU).
|
|
*/
|
|
typedef struct ColiMetalMoeHandle ColiMetalMoeHandle;
|
|
ColiMetalMoeHandle* coli_metal_moe_block_begin(int nb, int D, int Iinter, int fmt,
|
|
const void *const *g, const void *const *u, const void *const *d,
|
|
const float *const *gs, const float *const *us, const float *const *ds,
|
|
const float *xg, const int *xoff, const int *nr,
|
|
const int *rows, const float *rw);
|
|
int coli_metal_moe_block_end(ColiMetalMoeHandle *h, float *out);
|
|
|
|
#ifdef __cplusplus
|
|
}
|
|
#endif
|
|
|
|
#endif
|