Skip to content

Commit 3a53020

Browse files
unamedkrclaude
andcommitted
Metal MoE: hybrid GPU/CPU implemented, disabled (per-layer sync too slow)
Phase 1+2 (gate+up+SwiGLU) on GPU works correctly. Phase 3 (IQ2_S down) hangs — needs shader fix. Hybrid approach: GPU Phase 1+2 + CPU down — functional but slow due to per-layer Metal commit+wait synchronization overhead. Metal infrastructure fully in place for future IQ2_S fix. CPU fused IQ2 dot remains fastest: 3.7 tok/s (100 tokens). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c35a454 commit 3a53020

3 files changed

Lines changed: 114 additions & 69 deletions

File tree

include/turboquant/tq_gguf.h

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -289,23 +289,31 @@ void tq_moe_advise(const tq_moe_layer_t* layer,
289289
/* ============================================================
290290
* Fused MoE Metal GPU dispatch
291291
*
292-
* Processes ALL active experts for one MoE layer in 3 GPU
293-
* dispatches instead of per-expert per-matmul dispatch:
294-
* Phase 1: gate + up projections (all experts, parallel)
295-
* Phase 2: SwiGLU activation (all experts, parallel)
296-
* Phase 3: down projection + weighted accumulate
292+
* Hybrid GPU/CPU MoE dispatch:
293+
* Phase 1: gate + up projections on GPU (all experts, parallel)
294+
* Phase 2: SwiGLU activation on GPU (all experts, parallel)
295+
* Phase 3: down projection + weighted accumulate on CPU
296+
* (IQ2_S shader hangs on Metal; CPU fallback is reliable)
297297
*
298-
* Requires Apple Silicon with Metal. Returns -1 if unavailable.
298+
* GPU handles the larger gate+up matmuls (~2/3 of compute).
299+
* Returns 1 = partial (hb_output filled, caller does down+accum on CPU).
300+
* Returns -1 if unavailable.
299301
* ============================================================ */
300302

301303
/* Check if fused MoE Metal dispatch is available */
302304
int tq_metal_moe_available(void);
303305

304-
/* Fused MoE forward: 3 Metal dispatches for all active experts.
305-
* Returns 0 on success, -1 on failure (caller falls back to CPU). */
306+
/* Fused MoE forward: GPU handles gate+up+SwiGLU (Phases 1+2).
307+
* Returns:
308+
* 0 = full success (all phases on GPU, output[] filled)
309+
* 1 = partial success: gate+up+SwiGLU done on GPU,
310+
* hb_output[] filled with [num_active * expert_dim] SwiGLU'd activations,
311+
* caller must do down projection + accumulate on CPU.
312+
* -1 = failure (caller falls back to full CPU path). */
306313
int tq_metal_moe_forward(
307314
const float* input,
308315
float* output,
316+
float* hb_output, /* [num_active * expert_dim] — GPU writes SwiGLU results here */
309317
const void* weight_base,
310318
size_t weight_size,
311319
const uint64_t* gate_offsets,

src/backend/metal/tq_metal_dispatch.m

Lines changed: 62 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -756,10 +756,12 @@ int tq_metal_moe_available(void) {
756756
}
757757

758758
/**
759-
* Fused MoE forward: 3 dispatches for all active experts.
759+
* Hybrid GPU/CPU MoE forward: GPU does gate+up+SwiGLU (Phases 1+2),
760+
* CPU handles down projection (Phase 3 IQ2_S shader hangs on Metal).
760761
*
761762
* @param input Input hidden state [hidden_dim] (CPU pointer, will be uploaded)
762-
* @param output Output hidden state [hidden_dim] (CPU pointer, will be downloaded)
763+
* @param output Output hidden state [hidden_dim] (unused in hybrid mode)
764+
* @param hb_output SwiGLU'd activations [num_active * expert_dim] (filled by GPU)
763765
* @param weight_base Base pointer for all expert weights (mmap'd GGUF data)
764766
* @param weight_size Total size of weight region (for zero-copy buffer)
765767
* @param gate_offsets Byte offsets from weight_base to each active expert's gate weights [num_active]
@@ -775,11 +777,13 @@ int tq_metal_moe_available(void) {
775777
* @param gate_types_in Per-expert gate quant types [num_active] (NULL = use weight_type for all)
776778
* @param up_types_in Per-expert up quant types [num_active] (NULL = use weight_type for all)
777779
* @param down_types_in Per-expert down quant types [num_active] (NULL = use weight_type for all)
778-
* @return 0 on success, -1 on failure
780+
* @return 1 on partial success (hb_output filled, caller does down+accum on CPU),
781+
* -1 on failure
779782
*/
780783
int tq_metal_moe_forward(
781784
const float* input,
782785
float* output,
786+
float* hb_output,
783787
const void* weight_base,
784788
size_t weight_size,
785789
const uint64_t* gate_offsets,
@@ -812,11 +816,21 @@ int tq_metal_moe_forward(
812816
params.num_experts_total = num_experts_total;
813817
params.gate_type = (uint32_t)weight_type;
814818

815-
/* Default block geometry uses IQ2_XXS (66 bytes per 256 elements) */
819+
/* Block geometry: blocks_per_row is type-independent (always 256 elements/block).
820+
* row_bytes_gate/down use IQ2_XXS (66) as default; shader overrides per-expert
821+
* for IQ2_S (82) via the gate_types/down_types arrays. */
816822
params.blocks_per_row_gate = hidden_dim / 256;
817-
params.row_bytes_gate = params.blocks_per_row_gate * 66;
818823
params.blocks_per_row_down = expert_dim / 256;
819-
params.row_bytes_down = params.blocks_per_row_down * 66;
824+
825+
/* Determine default row bytes from first expert's type */
826+
int first_gate_type = gate_types_in ? gate_types_in[0] : weight_type;
827+
int first_down_type = down_types_in ? down_types_in[0] : weight_type;
828+
int gate_blk_bytes = (first_gate_type == 22) ? 82 : 66; /* IQ2_S=82, IQ2_XXS=66 */
829+
int down_blk_bytes = (first_down_type == 22) ? 82 : 66;
830+
params.row_bytes_gate = params.blocks_per_row_gate * gate_blk_bytes;
831+
params.row_bytes_down = params.blocks_per_row_down * down_blk_bytes;
832+
params.gate_row_bytes = params.row_bytes_gate;
833+
params.down_row_bytes = params.row_bytes_down;
820834

821835
for (int k = 0; k < num_active; k++) {
822836
params.active_experts[k] = active_expert_ids[k];
@@ -850,11 +864,8 @@ int tq_metal_moe_forward(
850864
options:MTLResourceStorageModeShared];
851865
if (!gate_buf || !up_buf) return -1;
852866

853-
/* --- Create output buffer --- */
854-
size_t output_bytes = (size_t)hidden_dim * sizeof(float);
855-
id<MTLBuffer> output_buf = [tq_mtl_device newBufferWithLength:output_bytes
856-
options:MTLResourceStorageModeShared];
857-
if (!output_buf) return -1;
867+
/* output_buf removed: hybrid mode returns SwiGLU results via hb_output,
868+
* down projection + accumulate happens on CPU in tq_moe.c */
858869

859870
/* --- Create params buffer --- */
860871
id<MTLBuffer> params_buf = [tq_mtl_device newBufferWithBytes:&params
@@ -866,16 +877,10 @@ int tq_metal_moe_forward(
866877
id<MTLCommandBuffer> cmdBuf = [tq_mtl_queue commandBuffer];
867878
if (!cmdBuf) return -1;
868879

869-
/* Shared memory sizes:
870-
* Phase 1 (gate_up): hidden_dim floats for input + 8 floats for SIMD sums
871-
* Phase 3 (down): expert_dim floats for hb + 8 floats for SIMD sums */
880+
/* Shared memory for Phase 1 (gate_up): hidden_dim floats for input + 8 for SIMD sums */
872881
NSUInteger shared_phase1 = ((NSUInteger)hidden_dim + 8) * sizeof(float);
873-
NSUInteger shared_phase3 = ((NSUInteger)expert_dim + 8) * sizeof(float);
874882
NSUInteger max_shared = [tq_mtl_device maxThreadgroupMemoryLength];
875-
876-
/* Cap shared memory to device limit */
877883
if (shared_phase1 > max_shared) shared_phase1 = max_shared;
878-
if (shared_phase3 > max_shared) shared_phase3 = max_shared;
879884

880885
/* ======== Phase 1: gate + up (fused) ======== */
881886
{
@@ -908,6 +913,38 @@ int tq_metal_moe_forward(
908913
}
909914
NSLog(@"TurboQuant MoE: Phase 1 (gate+up) completed OK");
910915

916+
#ifdef TQ_MOE_DEBUG_VALIDATE
917+
/* === Debug: compare GPU gate output for expert 0 vs CPU tq_matmul_gguf === */
918+
{
919+
/* tq_matmul_gguf declared in tq_gguf.h (already included) */
920+
float* gpu_gate = (float*)[gate_buf contents];
921+
float* cpu_gate = (float*)malloc((size_t)expert_dim * sizeof(float));
922+
if (cpu_gate) {
923+
/* CPU matmul for expert 0's gate weights */
924+
const uint8_t* gate_w = (const uint8_t*)weight_base + gate_offsets[0];
925+
tq_ggml_dtype gt0 = gate_types_in ? (tq_ggml_dtype)gate_types_in[0]
926+
: (tq_ggml_dtype)weight_type;
927+
tq_matmul_gguf(cpu_gate, input, gate_w, gt0, expert_dim, hidden_dim);
928+
929+
/* Compare first 8 and last 8 values */
930+
NSLog(@"TurboQuant MoE DEBUG: gate expert 0 comparison (first 8):");
931+
float max_err = 0.0f;
932+
for (int i = 0; i < expert_dim; i++) {
933+
float err = fabsf(gpu_gate[i] - cpu_gate[i]);
934+
if (err > max_err) max_err = err;
935+
if (i < 8 || i >= expert_dim - 4) {
936+
NSLog(@" [%d] GPU=%.6f CPU=%.6f err=%.6f", i, gpu_gate[i], cpu_gate[i], err);
937+
}
938+
}
939+
NSLog(@"TurboQuant MoE DEBUG: gate max_err=%.6f across %d elements", max_err, expert_dim);
940+
if (max_err > 0.01f) {
941+
NSLog(@"TurboQuant MoE DEBUG: *** MISMATCH DETECTED *** — weight offset or decoding bug");
942+
}
943+
free(cpu_gate);
944+
}
945+
}
946+
#endif /* TQ_MOE_DEBUG_VALIDATE */
947+
911948
/* --- New command buffer for Phase 2 --- */
912949
cmdBuf = [tq_mtl_queue commandBuffer];
913950
if (!cmdBuf) return -1;
@@ -941,42 +978,13 @@ int tq_metal_moe_forward(
941978
}
942979
NSLog(@"TurboQuant MoE: Phase 2 (SwiGLU) completed OK");
943980

944-
/* --- New command buffer for Phase 3 --- */
945-
cmdBuf = [tq_mtl_queue commandBuffer];
946-
if (!cmdBuf) return -1;
947-
948-
/* ======== Phase 3: down projection + weighted accumulation ======== */
949-
{
950-
id<MTLComputeCommandEncoder> enc = [cmdBuf computeCommandEncoder];
951-
if (!enc) return -1;
952-
953-
[enc setComputePipelineState:tq_pipe_moe_down_accum];
954-
[enc setBuffer:weight_buf offset:0 atIndex:0];
955-
[enc setBuffer:gate_buf offset:0 atIndex:1]; /* hb_all (post-SwiGLU) */
956-
[enc setBuffer:output_buf offset:0 atIndex:2];
957-
[enc setBuffer:params_buf offset:0 atIndex:3];
958-
[enc setThreadgroupMemoryLength:shared_phase3 atIndex:0];
959-
960-
/* One threadgroup per output row */
961-
NSUInteger n_tgs = (NSUInteger)hidden_dim;
962-
MTLSize gridSize = MTLSizeMake(n_tgs, 1, 1);
963-
MTLSize tgSize = MTLSizeMake(TQ_MATMUL_TG_SIZE, 1, 1);
964-
[enc dispatchThreadgroups:gridSize threadsPerThreadgroup:tgSize];
965-
[enc endEncoding];
966-
}
967-
968-
/* --- Phase 3: commit and wait --- */
969-
[cmdBuf commit];
970-
[cmdBuf waitUntilCompleted];
971-
972-
if (cmdBuf.status == MTLCommandBufferStatusError) {
973-
NSLog(@"TurboQuant MoE: Phase 3 (down+accum) FAILED: %@", cmdBuf.error);
974-
return -1;
975-
}
976-
NSLog(@"TurboQuant MoE: Phase 3 (down+accum) completed OK");
977-
978-
memcpy(output, [output_buf contents], output_bytes);
979-
return 0;
981+
/* ======== Phase 3: SKIP GPU down projection (IQ2_S shader hangs) ========
982+
* Instead, copy the SwiGLU'd activations from gate_buf back to CPU.
983+
* The caller (tq_moe.c) will do down projection + accumulate on CPU. */
984+
memcpy(hb_output, [gate_buf contents], inter_bytes);
985+
NSLog(@"TurboQuant MoE: Hybrid — Phase 1+2 on GPU done, returning hb for CPU down proj");
986+
(void)output; /* unused in hybrid mode */
987+
return 1; /* partial success: hb_output filled, caller does down+accum */
980988
}
981989
}
982990

src/engine/tq_moe.c

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -662,16 +662,17 @@ void tq_moe_forward(const tq_moe_layer_t* layer,
662662
#ifdef TQ_HAS_METAL
663663
extern int tq_metal_moe_available(void);
664664
extern int tq_metal_moe_forward(
665-
const float* input, float* output,
665+
const float* input, float* output, float* hb_output,
666666
const void* weight_base, size_t weight_size,
667667
const uint64_t* gate_offsets, const uint64_t* up_offsets, const uint64_t* down_offsets,
668668
const int* active_expert_ids, const float* expert_routing_weights,
669669
int num_active, int expert_dim, int hidden_dim, int num_experts_total, int weight_type,
670670
const int* gate_types, const int* up_types, const int* down_types);
671671

672-
/* Fused Metal MoE — re-enabled for per-phase hang isolation debugging.
673-
* Each phase runs with waitUntilCompleted + NSLog to identify which phase hangs. */
674-
if (tq_metal_moe_available() && num_active > 0) {
672+
/* Metal MoE disabled: IQ2_S Phase 3 hangs, hybrid too slow.
673+
* CPU fused IQ2 dot at 3.7 tok/s is the current fastest path.
674+
* GPU acceleration needs IQ2_S shader fix or Q4_K model format. */
675+
if (0 && tq_metal_moe_available() && num_active > 0) {
675676
/* Check that all active experts use IQ2_XXS and have valid weights */
676677
int can_fuse = 1;
677678
const void* base_ptr = NULL;
@@ -743,8 +744,12 @@ void tq_moe_forward(const tq_moe_layer_t* layer,
743744
per_down_types[k] = (int)exp->down_type;
744745
}
745746

747+
/* Allocate buffer for GPU SwiGLU results [num_active * expert_dim] */
748+
float* hb_gpu = (float*)malloc((size_t)num_active * (size_t)expert_dim * sizeof(float));
749+
if (!hb_gpu) goto moe_cpu_fallback;
750+
746751
int rc = tq_metal_moe_forward(
747-
input, output,
752+
input, output, hb_gpu,
748753
base_ptr, base_size,
749754
gate_offs, up_offs, down_offs,
750755
expert_ids, routing_w,
@@ -753,15 +758,39 @@ void tq_moe_forward(const tq_moe_layer_t* layer,
753758
0, /* weight_type=0 means use per-expert types */
754759
per_gate_types, per_up_types, per_down_types);
755760

761+
if (rc == 1) {
762+
/* Hybrid: GPU did gate+up+SwiGLU, we do down+accum on CPU */
763+
for (int k = 0; k < num_active; k++) {
764+
int eid = state->top_experts[k];
765+
float w = state->expert_weights[k];
766+
const tq_expert_weights_t* exp = &layer->experts[eid];
767+
float* hb_k = hb_gpu + k * expert_dim;
768+
769+
/* down = hb_k @ w_down^T -> [hidden_dim] */
770+
tq_matmul_gguf(state->expert_out, hb_k,
771+
exp->w_down, exp->down_type,
772+
hidden_dim, expert_dim);
773+
774+
/* Weighted accumulation: output += weight * down_proj */
775+
for (int i = 0; i < hidden_dim; i++)
776+
output[i] += w * state->expert_out[i];
777+
}
778+
free(hb_gpu);
779+
goto moe_shared_expert;
780+
}
781+
free(hb_gpu);
756782
if (rc == 0) {
757-
/* Fused MoE succeeded — skip to shared expert */
783+
/* Full GPU success (unlikely in hybrid mode, but handle it) */
758784
goto moe_shared_expert;
759785
}
760-
/* else: fall through to per-expert CPU path */
786+
/* else: rc == -1, fall through to per-expert CPU path */
761787
}
762788
}
763789
#endif /* TQ_HAS_METAL */
764790

791+
#ifdef TQ_HAS_METAL
792+
moe_cpu_fallback: ;
793+
#endif
765794
/* Step 3: For each selected expert, compute SwiGLU FFN and accumulate */
766795
for (int k = 0; k < num_active; k++) {
767796
int eid = state->top_experts[k];

0 commit comments

Comments
 (0)