mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-09-06 17:57:44 +02:00
sycl: fuse UNARY(silu|sigmoid|softplus) + MUL (#26411)
Measured on Arc Pro B70 (Battlemage), Qwen3.6-27B Q4_K_M, -fa on, f16 KV, -b 2048 -ub 2048, llama-bench -r 3, three interleaved A/B rounds: pp2048 1014.70 -> 1018.56 t/s (+0.38%, within run-to-run spread) tg128 23.73 -> 23.86 t/s (+0.57%) tg128 @ d4096 22.71 -> 22.86 t/s (+0.62%)
This commit is contained in:
@@ -3695,6 +3695,117 @@ struct test_relu_sqr : public test_case {
|
||||
}
|
||||
};
|
||||
|
||||
// GGML_OP_UNARY(SILU|SIGMOID|SOFTPLUS) + GGML_OP_MUL (fused operation).
|
||||
// `layout` and `tail` are used for fallback cases where fusion must be skipped
|
||||
struct test_unary_mul : public test_case {
|
||||
const ggml_unary_op op;
|
||||
const ggml_type type;
|
||||
const std::array<int64_t, 4> ne;
|
||||
const bool swap; // unary result is the second MUL operand
|
||||
const std::string layout; // operand layout, see build_graph()
|
||||
const std::string tail; // extra consumer past the MUL, see build_graph()
|
||||
|
||||
std::string op_desc(ggml_tensor * t) override {
|
||||
GGML_UNUSED(t);
|
||||
return std::string(ggml_unary_op_name(op)) + "_MUL";
|
||||
}
|
||||
|
||||
bool run_whole_graph() override { return true; }
|
||||
|
||||
double max_nmse_err() override {
|
||||
// the fused kernel elides the rounding of the unary result that the CPU chain
|
||||
// performs; relax the tolerance to match that drift
|
||||
switch (type) {
|
||||
case GGML_TYPE_F16: return 5e-5;
|
||||
default: return 1e-7;
|
||||
}
|
||||
}
|
||||
|
||||
std::string vars() override {
|
||||
return VARS_TO_STR5(type, ne, swap, layout, tail);
|
||||
}
|
||||
|
||||
test_unary_mul(ggml_unary_op op,
|
||||
ggml_type type = GGML_TYPE_F32,
|
||||
std::array<int64_t, 4> ne = {128, 2, 2, 2},
|
||||
bool swap = false,
|
||||
std::string layout = "packed",
|
||||
std::string tail = "")
|
||||
: op(op), type(type), ne(ne), swap(swap), layout(std::move(layout)), tail(std::move(tail)) {}
|
||||
|
||||
// `ne` viewed out of a wider tensor: rows stay contiguous, but the stride exceeds the width
|
||||
ggml_tensor * padded(ggml_context * ctx, const char * name, int64_t mul0, int64_t off0) {
|
||||
std::array<int64_t, 4> ne_w = ne;
|
||||
ne_w[0] *= mul0;
|
||||
ggml_tensor * base = ggml_new_tensor(ctx, type, 4, ne_w.data());
|
||||
ggml_set_name(base, name);
|
||||
return ggml_view_4d(ctx, base, ne[0], ne[1], ne[2], ne[3],
|
||||
base->nb[1], base->nb[2], base->nb[3], off0 * base->nb[0]);
|
||||
}
|
||||
|
||||
ggml_tensor * build_graph(ggml_context * ctx) override {
|
||||
ggml_tensor * a = nullptr; // unary source
|
||||
ggml_tensor * b = nullptr; // other MUL operand
|
||||
|
||||
if (layout == "packed") {
|
||||
a = ggml_new_tensor(ctx, type, 4, ne.data());
|
||||
b = ggml_new_tensor(ctx, type, 4, ne.data());
|
||||
} else if (layout == "pad_unary") {
|
||||
a = padded(ctx, "a", 3, 0);
|
||||
b = ggml_new_tensor(ctx, type, 4, ne.data());
|
||||
} else if (layout == "pad_other") {
|
||||
a = ggml_new_tensor(ctx, type, 4, ne.data());
|
||||
b = padded(ctx, "b", 3, 0);
|
||||
} else if (layout == "halves") {
|
||||
// the shape the Conformer audio encoders build: one tensor split in two
|
||||
std::array<int64_t, 4> ne_w = ne;
|
||||
ne_w[0] *= 2;
|
||||
ggml_tensor * base = ggml_new_tensor(ctx, type, 4, ne_w.data());
|
||||
ggml_set_name(base, "base");
|
||||
b = ggml_view_4d(ctx, base, ne[0], ne[1], ne[2], ne[3], base->nb[1], base->nb[2], base->nb[3], 0);
|
||||
a = ggml_view_4d(ctx, base, ne[0], ne[1], ne[2], ne[3], base->nb[1], base->nb[2], base->nb[3],
|
||||
ne[0] * base->nb[0]);
|
||||
} else if (layout == "strided_dim1") {
|
||||
// contiguous rows but a strided dim 1: not ggml_is_contiguous_1, must not fuse
|
||||
std::array<int64_t, 4> ne_w = ne;
|
||||
ne_w[1] *= 3;
|
||||
ggml_tensor * base = ggml_new_tensor(ctx, type, 4, ne_w.data());
|
||||
ggml_set_name(base, "a");
|
||||
a = ggml_view_4d(ctx, base, ne[0], ne[1], ne[2], ne[3], base->nb[1], base->nb[2], base->nb[3], 0);
|
||||
b = ggml_new_tensor(ctx, type, 4, ne.data());
|
||||
} else if (layout == "bcast") {
|
||||
a = ggml_new_tensor(ctx, type, 4, ne.data());
|
||||
b = ggml_new_tensor_4d(ctx, type, ne[0], 1, 1, 1);
|
||||
} else {
|
||||
GGML_ABORT("unknown layout %s", layout.c_str());
|
||||
}
|
||||
ggml_set_name(a, "a");
|
||||
ggml_set_name(b, "b");
|
||||
|
||||
ggml_tensor * u = ggml_unary(ctx, a, op);
|
||||
ggml_set_name(u, "unary");
|
||||
|
||||
// a broadcasting operand can only be the second one
|
||||
const bool second = swap && layout != "bcast";
|
||||
ggml_tensor * out = second ? ggml_mul(ctx, b, u) : ggml_mul(ctx, u, b);
|
||||
|
||||
if (tail == "reuse") {
|
||||
// a second read of the unary result must block the fusion
|
||||
ggml_set_name(out, "mul");
|
||||
out = ggml_add(ctx, out, u);
|
||||
} else if (tail == "consumer") {
|
||||
// fusion still applies; catches a dispatcher that skips one node too many
|
||||
ggml_set_name(out, "mul");
|
||||
out = ggml_add(ctx, out, b);
|
||||
} else if (!tail.empty()) {
|
||||
GGML_ABORT("unknown tail %s", tail.c_str());
|
||||
}
|
||||
ggml_set_name(out, "out");
|
||||
|
||||
return out;
|
||||
}
|
||||
};
|
||||
|
||||
// SNAKE activation fusion: y = x + sin(a*x)^2 * inv_b
|
||||
// CUDA backend matches the naive 5-op chain (mul, sin, sqr, mul, add)
|
||||
// and dispatches a single fused kernel.
|
||||
@@ -8065,6 +8176,25 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_eval() {
|
||||
test_cases.emplace_back(new test_relu_sqr(type, { 5, 7, 11, 13 }));
|
||||
}
|
||||
|
||||
// fused unary + mul (gated activations that are not expressed as GGML_OP_GLU)
|
||||
for (ggml_unary_op op : { GGML_UNARY_OP_SILU, GGML_UNARY_OP_SIGMOID, GGML_UNARY_OP_SOFTPLUS }) {
|
||||
for (ggml_type type : { GGML_TYPE_F16, GGML_TYPE_F32 }) {
|
||||
for (bool swap : { false, true }) {
|
||||
test_cases.emplace_back(new test_unary_mul(op, type, { 128, 2, 2, 2 }, swap));
|
||||
}
|
||||
test_cases.emplace_back(new test_unary_mul(op, type, { 5, 7, 11, 13 }));
|
||||
test_cases.emplace_back(new test_unary_mul(op, type, { 128, 2, 2, 2 }, false, "pad_unary"));
|
||||
// a view only stays out from between the two ops when the unary result is second
|
||||
test_cases.emplace_back(new test_unary_mul(op, type, { 128, 2, 2, 2 }, true, "pad_other"));
|
||||
test_cases.emplace_back(new test_unary_mul(op, type, { 128, 2, 2, 2 }, true, "halves"));
|
||||
test_cases.emplace_back(new test_unary_mul(op, type, { 128, 2, 2, 2 }, false, "packed", "consumer"));
|
||||
// must not fuse
|
||||
test_cases.emplace_back(new test_unary_mul(op, type, { 128, 2, 2, 2 }, false, "strided_dim1"));
|
||||
test_cases.emplace_back(new test_unary_mul(op, type, { 128, 2, 2, 2 }, false, "bcast"));
|
||||
test_cases.emplace_back(new test_unary_mul(op, type, { 128, 2, 2, 2 }, false, "packed", "reuse"));
|
||||
}
|
||||
}
|
||||
|
||||
// SNAKE activation fusion: x + sin(a*x)^2 * inv_b
|
||||
for (ggml_type type : { GGML_TYPE_F32, GGML_TYPE_F16, GGML_TYPE_BF16 }) {
|
||||
test_cases.emplace_back(new test_snake_fuse(type, { 5, 7, 1, 1})); // primes sub-block
|
||||
|
||||
Reference in New Issue
Block a user