Measured on Arc Pro B70 (Battlemage, Level Zero), llama-bench -r 20, two
interleaved rounds, tg128:
qwen2.5-3B-Instruct Q4_K_M 154.18 -> 158.53 t/s +2.8%
gemma-2-2b-it Q4_K_M 162.45 -> 165.62 t/s +2.0%
llama-batched-bench on qwen2.5-3B, S_TG by batch size:
B=1 142.72 -> 147.57 t/s +3.4%
B=2 243.72 -> 268.26 t/s +10.1%
B=4 359.58 -> 398.02 t/s +10.7%
B=8 449.75 -> 505.63 t/s +12.4%
170 lines
6.3 KiB
C++
170 lines
6.3 KiB
C++
#include "fusion.hpp"
|
|
|
|
#include <algorithm>
|
|
|
|
// 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<enum ggml_op> ops,
|
|
std::initializer_list<enum ggml_unary_op> 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;
|
|
}
|