#include "fusion.hpp" #include // mul_mat(gate) + mul_mat(up) + GLU: graph shape and tensor properties only. Backend state // (weight layout, split buffers, DMMV) is checked by ggml_sycl_mul_mat_glu_mmvq_fused(). static bool ggml_sycl_should_fuse_mul_mat_glu(const ggml_tensor * gate, const ggml_tensor * up, const ggml_tensor * glu) { // the fused epilogue implements these two; the rest fall back to the standalone GLU kernels const ggml_glu_op glu_op = ggml_get_glu_op(glu); if (glu_op != GGML_GLU_OP_SWIGLU && glu_op != GGML_GLU_OP_GEGLU) { return false; } // the kernel always treats src[0] as the activated operand and src[1] as the multiplier if (ggml_get_op_params_i32(glu, 1) /* swapped */) { return false; } const ggml_tensor * wu = up->src[0]; const ggml_tensor * wg = gate->src[0]; const ggml_tensor * act = up->src[1]; // one set of block offsets and one quantized activation must serve both weights if (wu->type != wg->type || !ggml_are_same_shape(wu, wg) || !ggml_are_same_stride(wu, wg)) { return false; } if (act != gate->src[1]) { return false; } // only q4_K has a fused reorder GEMV so far, and it walks whole super-blocks if (wu->type != GGML_TYPE_Q4_K || wu->ne[0] % QK_K != 0) { return false; } // one 2D reorder-layout matrix in, a plain column stride out: no broadcast or padding if (!ggml_is_contiguous(wu) || !ggml_is_contiguous(wg) || !ggml_is_contiguous(act) || !ggml_is_contiguous(glu)) { return false; } if (act->type != GGML_TYPE_F32 || glu->type != GGML_TYPE_F32) { return false; } if (act->ne[2] != 1 || act->ne[3] != 1 || wu->ne[2] != 1 || wu->ne[3] != 1) { return false; } // the kernel writes rows [0, wu->ne[1]) of each glu column, strided by glu->ne[0] if (glu->ne[0] != wu->ne[1] || glu->ne[1] != act->ne[1]) { return false; } // mat-vec only: one column per decoded token, up to the batch the reorder kernels cover if (act->ne[1] > MMVQ_MAX_BATCH_SIZE) { return false; } return true; } bool ggml_sycl_can_fuse(const ggml_cgraph * cgraph, int node_idx, std::initializer_list ops, std::initializer_list unary_ops) { #ifndef NDEBUG const size_t num_unary = std::count(ops.begin(), ops.end(), GGML_OP_UNARY); GGML_ASSERT(unary_ops.size() == num_unary); #endif if (!g_ggml_sycl_enable_fusion) { return false; } // gate and up are siblings, not a chain, so ggml_can_fuse cannot express this: use the // subgraph form with the GLU as the only materialised output. if (ops.size() == 3 && ops.begin()[0] == GGML_OP_MUL_MAT && ops.begin()[1] == GGML_OP_MUL_MAT && ops.begin()[2] == GGML_OP_GLU) { if (!ggml_can_fuse_subgraph(cgraph, node_idx, ops, { node_idx + 2 })) { return false; } const ggml_tensor * glu = cgraph->nodes[node_idx + 2]; const ggml_tensor * gate = glu->src[0]; const ggml_tensor * up = glu->src[1]; // don't assume which of the two mat-muls is the gate; infer it from the GLU's operands const bool ok = (gate == cgraph->nodes[node_idx] && up == cgraph->nodes[node_idx + 1]) || (gate == cgraph->nodes[node_idx + 1] && up == cgraph->nodes[node_idx]); if (!ok) { return false; } return ggml_sycl_should_fuse_mul_mat_glu(gate, up, glu); } if (!ggml_can_fuse(cgraph, node_idx, ops)) { return false; } if (ops.size() == 2 && ops.begin()[0] == GGML_OP_RMS_NORM && ops.begin()[1] == GGML_OP_MUL) { const ggml_tensor * rms_norm = cgraph->nodes[node_idx]; const ggml_tensor * mul = cgraph->nodes[node_idx + 1]; GGML_ASSERT(rms_norm->src[0]->type == GGML_TYPE_F32); GGML_ASSERT(rms_norm->type == GGML_TYPE_F32); if (mul->src[0]->type != GGML_TYPE_F32 || mul->src[1]->type != GGML_TYPE_F32 || mul->type != GGML_TYPE_F32) { return false; } // if rms norm is the B operand, then we don't handle broadcast if (rms_norm == mul->src[1] && !ggml_are_same_shape(mul->src[0], rms_norm)) { return false; } const ggml_tensor * mul_w = (mul->src[0] == rms_norm) ? mul->src[1] : mul->src[0]; // the fused kernel indexes the weight as mul[col], so it must span ncols contiguously if (mul_w->ne[0] != rms_norm->ne[0] || mul_w->nb[0] != ggml_type_size(mul_w->type)) { return false; } if (!ggml_is_contiguous_rows(mul->src[0]) || !ggml_is_contiguous_rows(mul->src[1])) { return false; } return true; } if (ops.size() == 2 && ops.begin()[0] == GGML_OP_UNARY && ops.begin()[1] == GGML_OP_MUL && unary_ops.size() == 1) { const ggml_tensor * unary = cgraph->nodes[node_idx]; const ggml_tensor * mul = cgraph->nodes[node_idx + 1]; const ggml_unary_op unary_op = ggml_get_unary_op(unary); if (unary_op != unary_ops.begin()[0]) { return false; } // the ops ggml_sycl_op_unary_mul_fused() has a kernel for if (unary_op != GGML_UNARY_OP_SILU && unary_op != GGML_UNARY_OP_SIGMOID && unary_op != GGML_UNARY_OP_SOFTPLUS) { return false; } if (unary->type != GGML_TYPE_F32 && unary->type != GGML_TYPE_F16) { return false; } const ggml_tensor * other = (mul->src[0] == unary) ? mul->src[1] : mul->src[0]; if (other->type != unary->type) { return false; } // one row stride per source comes from nb[1], so rows must be contiguous and equally // shaped; the destination is written flat, so it must be fully contiguous if (!ggml_is_contiguous_1(unary->src[0]) || !ggml_is_contiguous_1(other) || !ggml_are_same_shape(other, unary) || !ggml_is_contiguous(mul)) { return false; } // the 32-bit fastdiv is inexact past 2^31; decline, the unfused path handles it if (ggml_nelements(mul) >= ((int64_t) 1 << 31)) { return false; } return true; } return false; }