#include "ggml-metal-common.h"
#include "ggml-metal-fusion.h"
#include "ggml.h"
#include "ggml-impl.h"
#include "ggml-backend-impl.h"
#include <vector>
static bool ggml_metal_fwht_supported_size(int64_t n, size_t max_tg_mem) {
if (n == 64 || n == 128 || n == 256 || n == 512) {
return true;
}
if (n == 1024 || n == 2048 || n == 4096 || n == 8192) {
return (size_t) n * sizeof(float) <= max_tg_mem;
}
return false;
}
bool ggml_metal_op_mul_mat_use_fwht(const struct ggml_tensor * op, size_t max_tg_mem) {
return ggml_get_op_params_i32(op, 1) == GGML_HINT_SRC0_IS_HADAMARD && op->type == GGML_TYPE_F32 &&
(op->src[1]->type == GGML_TYPE_F32 || op->src[1]->type == GGML_TYPE_F16) && ggml_is_contiguous(op->src[1]) &&
ggml_is_contiguous(op) && ggml_are_same_shape(op->src[1], op) &&
ggml_metal_fwht_supported_size(op->src[1]->ne[0], max_tg_mem);
}
bool ggml_metal_op_mul_mat_use_mm(const struct ggml_tensor * op, bool has_simdgroup_mm) {
const int64_t ne00 = op->src[0]->ne[0];
const int64_t ne11 = op->src[1]->ne[1];
return !ggml_is_transposed(op->src[0]) &&
!ggml_is_transposed(op->src[1]) &&
has_simdgroup_mm && ne00 >= 64 && ne11 > 8;
}
bool ggml_metal_op_mul_mat_id_use_mm(const struct ggml_tensor * op, bool has_simdgroup_mm) {
const int64_t ne00 = op->src[0]->ne[0];
const int64_t ne21 = op->src[2]->ne[1];
return has_simdgroup_mm && ne00 >= 64 && ne21 >= 32;
}
static constexpr int64_t GGML_METAL_MMA_ROWS_MAX = 16;
static constexpr int64_t GGML_METAL_MMA_TILE_ROWS = 8;
static constexpr int64_t GGML_METAL_MMA_K_CHUNK = 64;
enum ggml_metal_mma_kind ggml_metal_mul_mv_mma_kind(enum ggml_type type, int rt) {
if (type == GGML_TYPE_Q4_0 || (type == GGML_TYPE_Q8_0 && rt == 1)) {
return GGML_METAL_MMA_KIND_BLK;
}
return type == GGML_TYPE_Q5_K ? GGML_METAL_MMA_KIND_Q5_K : GGML_METAL_MMA_KIND_GEN;
}
int ggml_metal_mul_mv_mma_rt(const struct ggml_tensor * op) {
return op->src[1]->ne[1] > GGML_METAL_MMA_TILE_ROWS ? 2 : 1;
}
static bool ggml_metal_mul_mv_mma_type_supported(enum ggml_type type) {
switch (type) {
case GGML_TYPE_F32:
case GGML_TYPE_F16:
case GGML_TYPE_Q4_0:
case GGML_TYPE_Q4_1:
case GGML_TYPE_Q5_0:
case GGML_TYPE_Q5_1:
case GGML_TYPE_Q8_0:
case GGML_TYPE_Q4_K:
case GGML_TYPE_Q5_K:
case GGML_TYPE_Q6_K:
case GGML_TYPE_BF16:
case GGML_TYPE_Q1_0:
case GGML_TYPE_Q2_0:
case GGML_TYPE_MXFP4:
case GGML_TYPE_Q2_K:
case GGML_TYPE_Q3_K:
case GGML_TYPE_IQ2_XXS:
case GGML_TYPE_IQ2_XS:
case GGML_TYPE_IQ2_S:
case GGML_TYPE_IQ3_XXS:
case GGML_TYPE_IQ3_S:
case GGML_TYPE_IQ1_S:
case GGML_TYPE_IQ1_M:
case GGML_TYPE_IQ4_NL:
case GGML_TYPE_IQ4_XS:
case GGML_TYPE_TQ2_0:
return true;
default:
return false;
}
}
int64_t ggml_metal_mul_mv_mma_k_step(enum ggml_type type, int rt) {
if (!ggml_metal_mul_mv_mma_type_supported(type)) {
return 0;
}
return ggml_metal_mul_mv_mma_kind(type, rt) == GGML_METAL_MMA_KIND_BLK ? ggml_blck_size(type) : GGML_METAL_MMA_K_CHUNK;
}
static bool ggml_metal_mul_mat_mma_type_ok(const struct ggml_tensor * op) {
const ggml_tensor * src0 = op->src[0];
const int64_t step = ggml_metal_mul_mv_mma_k_step(src0->type, ggml_metal_mul_mv_mma_rt(op));
return step > 0 && src0->ne[0] % step == 0 && src0->nb[0] == ggml_type_size(src0->type);
}
static int64_t ggml_metal_mul_mv_mma_rows_min(enum ggml_type type) {
switch (type) {
case GGML_TYPE_F32:
return 6;
case GGML_TYPE_TQ2_0:
return 5;
case GGML_TYPE_BF16:
return 4;
case GGML_TYPE_F16:
case GGML_TYPE_Q2_0:
case GGML_TYPE_Q4_K:
case GGML_TYPE_Q5_0:
case GGML_TYPE_Q5_1:
case GGML_TYPE_Q2_K:
case GGML_TYPE_IQ4_NL:
case GGML_TYPE_MXFP4:
return 3;
default:
return 2;
}
}
bool ggml_metal_op_mul_mat_use_mma(const struct ggml_tensor * op) {
const ggml_tensor * src0 = op->src[0];
const ggml_tensor * src1 = op->src[1];
const bool batch_ok = src1->ne[2] <= INT16_MAX && src1->ne[2]/src0->ne[2] <= INT16_MAX && src1->ne[3]/src0->ne[3] <= INT16_MAX;
return ggml_metal_mul_mat_mma_type_ok(op) && batch_ok &&
src1->type == GGML_TYPE_F32 && src1->ne[1] >= ggml_metal_mul_mv_mma_rows_min(src0->type) && src1->ne[1] <= GGML_METAL_MMA_ROWS_MAX &&
!ggml_is_transposed(src0) && !ggml_is_transposed(src1) &&
src1->nb[0] == sizeof(float) && src1->nb[1] % 16 == 0 && src1->nb[2] % 16 == 0 && src1->nb[3] % 16 == 0;
}
bool ggml_metal_op_mul_mat_may_use_mma(const struct ggml_tensor * op) {
return ggml_metal_mul_mv_mma_type_supported(op->src[0]->type) && op->src[1]->type == GGML_TYPE_F32;
}
struct ggml_mem_range {
uint64_t pb;
uint64_t p0; uint64_t p1;
ggml_mem_range_type pt;
};
struct ggml_mem_ranges {
std::vector<ggml_mem_range> ranges;
int debug = 0;
};
ggml_mem_ranges_t ggml_mem_ranges_init(int debug) {
auto * res = new ggml_mem_ranges;
res->ranges.reserve(256);
res->debug = debug;
return res;
}
void ggml_mem_ranges_free(ggml_mem_ranges_t mrs) {
delete mrs;
}
void ggml_mem_ranges_reset(ggml_mem_ranges_t mrs) {
mrs->ranges.clear();
}
static bool ggml_mem_ranges_add(ggml_mem_ranges_t mrs, ggml_mem_range mr) {
mrs->ranges.push_back(mr);
return true;
}
static ggml_mem_range ggml_mem_range_from_tensor(const ggml_tensor * tensor, ggml_mem_range_type pt) {
tensor = tensor->view_src ? tensor->view_src : tensor;
GGML_ASSERT(!tensor->view_src);
ggml_mem_range mr;
if (tensor->buffer) {
mr = {
(uint64_t) tensor->buffer,
(uint64_t) tensor->data,
(uint64_t) tensor->data + ggml_backend_buft_get_alloc_size(tensor->buffer->buft, tensor),
pt,
};
} else {
mr = {
(uint64_t) tensor,
0, 1024, pt,
};
};
return mr;
}
static ggml_mem_range ggml_mem_range_from_tensor_src(const ggml_tensor * tensor) {
return ggml_mem_range_from_tensor(tensor, MEM_RANGE_TYPE_SRC);
}
static ggml_mem_range ggml_mem_range_from_tensor_dst(const ggml_tensor * tensor) {
return ggml_mem_range_from_tensor(tensor, MEM_RANGE_TYPE_DST);
}
static bool ggml_mem_ranges_add_src(ggml_mem_ranges_t mrs, const ggml_tensor * tensor) {
GGML_ASSERT(tensor);
ggml_mem_range mr = ggml_mem_range_from_tensor_src(tensor);
if (mrs->debug > 2) {
GGML_LOG_DEBUG("%s: add src range buf=%lld, [%lld, %lld)\n", __func__, mr.pb, mr.p0, mr.p1);
}
return ggml_mem_ranges_add(mrs, mr);
}
static bool ggml_mem_ranges_add_dst(ggml_mem_ranges_t mrs, const ggml_tensor * tensor) {
GGML_ASSERT(tensor);
ggml_mem_range mr = ggml_mem_range_from_tensor_dst(tensor);
if (mrs->debug > 2) {
GGML_LOG_DEBUG("%s: add dst range buf=%lld, [%lld, %lld)\n", __func__, mr.pb, mr.p0, mr.p1);
}
return ggml_mem_ranges_add(mrs, mr);
}
bool ggml_mem_ranges_add(ggml_mem_ranges_t mrs, const ggml_tensor * tensor) {
for (int i = 0; i < GGML_MAX_SRC; i++) {
if (tensor->src[i]) {
ggml_mem_ranges_add_src(mrs, tensor->src[i]);
}
}
return ggml_mem_ranges_add_dst(mrs, tensor);
}
static bool ggml_mem_ranges_check(ggml_mem_ranges_t mrs, ggml_mem_range mr) {
for (size_t i = 0; i < mrs->ranges.size(); i++) {
const auto & cmp = mrs->ranges[i];
if (mr.pb != cmp.pb) {
continue;
}
if (mr.pt == MEM_RANGE_TYPE_SRC && cmp.pt == MEM_RANGE_TYPE_SRC) {
continue;
}
if (mr.p0 < cmp.p1 && mr.p1 >= cmp.p0) {
if (mrs->debug > 2) {
GGML_LOG_DEBUG("%s: the %s range buf=%lld, [%lld, %lld) overlaps with a previous %s range buf=%lld, [%lld, %lld)\n",
__func__,
mr.pt == MEM_RANGE_TYPE_SRC ? "src" : "dst",
mr.pb, mr.p0, mr.p1,
cmp.pt == MEM_RANGE_TYPE_SRC ? "src" : "dst",
cmp.pb, cmp.p0, cmp.p1);
}
return false;
}
}
return true;
}
static bool ggml_mem_ranges_check_src(ggml_mem_ranges_t mrs, const ggml_tensor * tensor) {
GGML_ASSERT(tensor);
ggml_mem_range mr = ggml_mem_range_from_tensor_src(tensor);
const bool res = ggml_mem_ranges_check(mrs, mr);
return res;
}
static bool ggml_mem_ranges_check_dst(ggml_mem_ranges_t mrs, const ggml_tensor * tensor) {
GGML_ASSERT(tensor);
ggml_mem_range mr = ggml_mem_range_from_tensor_dst(tensor);
const bool res = ggml_mem_ranges_check(mrs, mr);
return res;
}
bool ggml_mem_ranges_check(ggml_mem_ranges_t mrs, const ggml_tensor * tensor) {
for (int i = 0; i < GGML_MAX_SRC; i++) {
if (tensor->src[i]) {
if (!ggml_mem_ranges_check_src(mrs, tensor->src[i])) {
return false;
}
}
}
return ggml_mem_ranges_check_dst(mrs, tensor);
}
struct node_info {
ggml_tensor * node;
std::vector<ggml_tensor *> fused;
ggml_op op() const {
return node->op;
}
const ggml_tensor * dst() const {
return fused.empty() ? node : fused.back();
}
bool is_empty() const {
return ggml_op_is_empty(node->op);
}
void add_fused(ggml_tensor * t) {
fused.push_back(t);
}
bool is_output(const ggml_tensor * t) const {
if (t == node) {
return true;
}
for (const auto * f : fused) {
if (t == f) {
return true;
}
}
return false;
}
};
static std::vector<int> ggml_metal_graph_optimize_reorder(const std::vector<node_info> & nodes) {
const auto & h_add = [](ggml_mem_ranges_t mrs, const node_info & node) {
for (int i = 0; i < GGML_MAX_SRC; i++) {
const ggml_tensor * src = node.node->src[i];
if (src && !node.is_output(src)) {
if (!ggml_mem_ranges_add_src(mrs, src)) {
return false;
}
}
}
for (const auto * fused : node.fused) {
for (int i = 0; i < GGML_MAX_SRC; i++) {
const ggml_tensor * src = fused->src[i];
if (src && !node.is_output(src)) {
if (!ggml_mem_ranges_add_src(mrs, src)) {
return false;
}
}
}
}
if (!ggml_mem_ranges_add_dst(mrs, node.node)) {
return false;
}
for (const auto * fused : node.fused) {
if (!ggml_mem_ranges_add_dst(mrs, fused)) {
return false;
}
}
return true;
};
const auto & h_check = [](ggml_mem_ranges_t mrs, const node_info & node) {
for (int i = 0; i < GGML_MAX_SRC; i++) {
const ggml_tensor * src = node.node->src[i];
if (src && !node.is_output(src)) {
if (!ggml_mem_ranges_check_src(mrs, src)) {
return false;
}
}
}
for (const auto * fused : node.fused) {
for (int i = 0; i < GGML_MAX_SRC; i++) {
const ggml_tensor * src = fused->src[i];
if (src && !node.is_output(src)) {
if (!ggml_mem_ranges_check_src(mrs, src)) {
return false;
}
}
}
}
if (!ggml_mem_ranges_check_dst(mrs, node.node)) {
return false;
}
for (const auto * fused : node.fused) {
if (!ggml_mem_ranges_check_dst(mrs, fused)) {
return false;
}
}
return true;
};
const auto & h_safe = [](ggml_op op) {
switch (op) {
case GGML_OP_MUL_MAT:
case GGML_OP_MUL_MAT_ID:
case GGML_OP_ROPE:
case GGML_OP_NORM:
case GGML_OP_RMS_NORM:
case GGML_OP_GROUP_NORM:
case GGML_OP_L2_NORM:
case GGML_OP_SUM_ROWS:
case GGML_OP_SSM_CONV:
case GGML_OP_SSM_SCAN:
case GGML_OP_CLAMP:
case GGML_OP_TRI:
case GGML_OP_DIAG:
case GGML_OP_MUL:
case GGML_OP_ADD:
case GGML_OP_SUB:
case GGML_OP_DIV:
case GGML_OP_GLU:
case GGML_OP_SCALE:
case GGML_OP_UNARY:
case GGML_OP_GET_ROWS:
case GGML_OP_SET_ROWS:
case GGML_OP_SET:
case GGML_OP_CPY:
case GGML_OP_CONT:
case GGML_OP_REPEAT:
return true;
default:
return ggml_op_is_empty(op);
}
};
const int n = nodes.size();
std::vector<int> res;
res.reserve(n);
std::vector<bool> used(n, false);
ggml_mem_ranges_t mrs0 = ggml_mem_ranges_init(0);
ggml_mem_ranges_t mrs1 = ggml_mem_ranges_init(0);
for (int i0 = 0; i0 < n; i0++) {
if (used[i0]) {
continue;
}
const auto & node0 = nodes[i0];
if (!node0.is_empty() && !h_check(mrs0, node0)) {
ggml_mem_ranges_reset(mrs1);
h_add(mrs1, node0);
constexpr int N_FORWARD = 64;
for (int i1 = i0 + 1; i1 < i0 + N_FORWARD && i1 < n; i1++) {
if (used[i1]) {
continue;
}
const auto & node1 = nodes[i1];
if (!h_safe(node1.op())) {
break;
}
const bool is_empty = node1.is_empty();
if ((is_empty || h_check(mrs0, node1)) && h_check(mrs1, node1)) {
h_add(mrs0, node1);
res.push_back(i1);
used[i1] = true;
} else {
h_add(mrs1, node1);
}
}
ggml_mem_ranges_reset(mrs0);
}
{
h_add(mrs0, node0);
res.push_back(i0);
}
}
ggml_mem_ranges_free(mrs0);
ggml_mem_ranges_free(mrs1);
return res;
}
void ggml_graph_optimize(ggml_cgraph * gf) {
const int n = gf->n_nodes;
std::vector<node_info> nodes;
nodes.reserve(gf->n_nodes);
for (int i = 0; i < n; i++) {
node_info node = {
gf->nodes[i],
{},
};
const int f = ggml_metal_fusion_max(gf, i);
for (int k = 1; k < f; k++) {
++i;
node.add_fused(gf->nodes[i]);
}
nodes.push_back(std::move(node));
}
#if 1
const auto order = ggml_metal_graph_optimize_reorder(nodes);
#else#endif
{
int j = 0;
for (const auto i : order) {
const auto & node = nodes[i];
gf->nodes[j++] = node.node;
for (auto * fused : node.fused) {
gf->nodes[j++] = fused;
}
}
}
}