Skip to content

Commit a37cb98

Browse files
authored
Merge pull request #66 from AMD-Ecosystem/rogarcia.roofline-graph-topology
roofline: emit per-launch topology edges for graph reconstruction
2 parents f886419 + c7060bd commit a37cb98

1 file changed

Lines changed: 108 additions & 1 deletion

File tree

ggml/src/ggml-cuda/ggml-cuda-roofline.cpp

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#include <cxxabi.h>
2525

2626
#include <algorithm>
27+
#include <array>
2728
#include <atomic>
2829
#include <cstdint>
2930
#include <cstdio>
@@ -80,6 +81,27 @@ struct dispatch {
8081
uint64_t duration_ns = 0;
8182
};
8283

84+
// Per-launch identity for graph reconstruction: the geometry record is shared across ops of the
85+
// same shape, so only these ids distinguish repeated layers. Ids are storage ids at the view_src
86+
// root, so a read of a view/reshape (which launches no kernel and has no row) still resolves to the
87+
// producing op. The consumer links each in_storage_id to the last prior launch whose out_storage_id
88+
// matches (last writer wins, which also resolves in-place ops).
89+
// One entry per external source (aligned 1:1 with in_storage_ids). Holds each operand's own
90+
// name/type/shape so the consumer can label and classify it (weight vs dynamic input, by name) --
91+
// which a fused row's head-only geometry record can't supply.
92+
struct src_operand {
93+
std::string name;
94+
std::string type; // ggml_type_name
95+
int64_t ne[4] = {0, 0, 0, 0};
96+
};
97+
98+
struct node_topology {
99+
uint64_t out_storage_id = 0;
100+
std::vector<uint64_t> in_storage_ids;
101+
std::vector<src_operand> in_operands; // aligned with in_storage_ids
102+
std::string name;
103+
};
104+
83105
std::mutex g_mutex;
84106
std::unordered_map<uint64_t, op_record> g_records; // geometry id -> geometry
85107
std::unordered_map<uint64_t, uint64_t> g_invocations; // invocation id -> geometry id
@@ -92,6 +114,7 @@ std::unordered_map<uint64_t, int64_t> g_invocation_experts; // i
92114
// (token, slot) routings that selected expert e; Σ = n_tokens*top_k). Emitted as tokens_per_expert
93115
// so the consumer can show the routing-load distribution (skew that drives MoE-GEMM padding).
94116
std::unordered_map<uint64_t, std::vector<int32_t>> g_invocation_expert_hist; // invocation id -> per-expert token counts
117+
std::unordered_map<uint64_t, node_topology> g_invocation_topology; // invocation id -> per-launch identity (edges)
95118
std::atomic<uint64_t> g_next_invocation{1};
96119
thread_local uint64_t g_current_invocation = 0; // id pushed by the last begin_op on this thread
97120

@@ -180,6 +203,18 @@ const ggml_tensor * roofline_storage(const ggml_tensor * t) {
180203
return t;
181204
}
182205

206+
// Capture one source operand's identity (name/type/shape) for the topology, so the consumer can
207+
// label and classify it directly. The name is taken from the storage root (a view of a model
208+
// weight keeps the weight's name), which is what makes weight-vs-input unambiguous.
209+
src_operand make_src_operand(const ggml_tensor * t) {
210+
src_operand op;
211+
const ggml_tensor * root = roofline_storage(t);
212+
op.name = root && root->name[0] ? root->name : (t->name[0] ? t->name : "");
213+
op.type = ggml_type_name(t->type);
214+
for (int d = 0; d < 4; d++) op.ne[d] = t->ne[d];
215+
return op;
216+
}
217+
183218
// Fill a record's geometry and single-node HBM byte fields from one ggml node.
184219
void fill_head_record(op_record & rec, const ggml_tensor * node) {
185220
const ggml_tensor * src0 = node->src[0];
@@ -384,7 +419,41 @@ void write_report() {
384419
if (!first) out << ",\n";
385420
first = false;
386421

387-
out << " {\"ggml_op\": \"" << rec.op << "\", ";
422+
out << " {\"ggml_op\": \"" << rec.op << "\", \"invocation\": " << invocation << ", ";
423+
// Per-launch identity for graph reconstruction: sort rows by invocation for execution
424+
// order, then link each in_tensor_id to the producing launch's out_tensor_id.
425+
auto topo_it = g_invocation_topology.find(invocation);
426+
if (topo_it != g_invocation_topology.end()) {
427+
out << "\"name\": \"";
428+
json_escape(out, topo_it->second.name);
429+
out << "\", \"out_storage_id\": " << topo_it->second.out_storage_id << ", \"in_storage_ids\": [";
430+
for (size_t j = 0; j < topo_it->second.in_storage_ids.size(); j++) {
431+
if (j) out << ", ";
432+
out << topo_it->second.in_storage_ids[j];
433+
}
434+
out << "], ";
435+
// Per-operand identity aligned 1:1 with in_storage_ids: the tensor name (unambiguous
436+
// weight-vs-input), its dtype and its shape -- correct even for a fused span's non-head
437+
// operands, which the shared geometry record's head-only src arrays do not describe.
438+
const auto & ops = topo_it->second.in_operands;
439+
out << "\"in_names\": [";
440+
for (size_t j = 0; j < ops.size(); j++) {
441+
if (j) out << ", ";
442+
out << "\""; json_escape(out, ops[j].name); out << "\"";
443+
}
444+
out << "], \"in_types\": [";
445+
for (size_t j = 0; j < ops.size(); j++) {
446+
if (j) out << ", ";
447+
out << "\"" << ops[j].type << "\"";
448+
}
449+
out << "], \"in_ne\": [";
450+
for (size_t j = 0; j < ops.size(); j++) {
451+
if (j) out << ", ";
452+
out << "[" << ops[j].ne[0] << ", " << ops[j].ne[1] << ", "
453+
<< ops[j].ne[2] << ", " << ops[j].ne[3] << "]";
454+
}
455+
out << "], ";
456+
}
388457
if (!rec.fused_nodes.empty()) {
389458
out << "\"fused_ops\": [";
390459
for (size_t k = 0; k < rec.fused_nodes.size(); k++) {
@@ -541,6 +610,7 @@ void ggml_cuda_roofline_reset(void) {
541610
g_invocations.clear();
542611
g_dispatches.clear();
543612
g_invocation_experts.clear();
613+
g_invocation_topology.clear();
544614
// g_next_invocation stays monotonic so a late warmup record cannot collide with a
545615
// post-reset invocation id; g_kernel_names is kept (code objects do not reload).
546616
}
@@ -579,10 +649,23 @@ void ggml_cuda_roofline_begin_op(const struct ggml_tensor * node, void * stream)
579649
if (!hist.empty()) g_invocation_expert_hist[invocation] = std::move(hist);
580650
}
581651

652+
// Per-launch identity for graph edges (overridden by fuse_ops for a fused span). Resolve to
653+
// storage roots so consumers of a view/reshape of this output still link back here.
654+
node_topology topo;
655+
topo.out_storage_id = (uint64_t) (uintptr_t) roofline_storage(node);
656+
topo.name = node->name;
657+
for (int j = 0; j < GGML_MAX_SRC; j++) {
658+
if (node->src[j]) {
659+
topo.in_storage_ids.push_back((uint64_t) (uintptr_t) roofline_storage(node->src[j]));
660+
topo.in_operands.push_back(make_src_operand(node->src[j]));
661+
}
662+
}
663+
582664
{
583665
std::lock_guard<std::mutex> lock(g_mutex);
584666
g_invocations.emplace(invocation, geometry_id);
585667
if (g_records.find(geometry_id) == g_records.end()) g_records.emplace(geometry_id, rec);
668+
g_invocation_topology.emplace(invocation, std::move(topo));
586669
}
587670

588671
// Tag the kernels launched until the next op with this invocation id. rocprofiler
@@ -632,10 +715,34 @@ void ggml_cuda_roofline_fuse_ops(const struct ggml_cgraph * cgraph, int node_idx
632715
for (int d = 0; d < 4; d++) geometry_id = hash_mix(geometry_id, (uint64_t) n->ne[d]);
633716
}
634717

718+
// Rebuild the launch identity for the whole span: the group produces the last node's output,
719+
// and reads only storages not written within the span (internal tensors are elided). Storage
720+
// roots so a src that views an internal output is recognised as internal.
721+
std::unordered_set<uint64_t> internal;
722+
for (int j = node_idx; j < node_idx + node_count; ++j) {
723+
internal.insert((uint64_t) (uintptr_t) roofline_storage(cgraph->nodes[j]));
724+
}
725+
node_topology topo;
726+
topo.out_storage_id = (uint64_t) (uintptr_t) roofline_storage(cgraph->nodes[node_idx + node_count - 1]);
727+
topo.name = head->name;
728+
std::unordered_set<uint64_t> seen;
729+
for (int j = node_idx; j < node_idx + node_count; ++j) {
730+
const ggml_tensor * n = cgraph->nodes[j];
731+
for (int s = 0; s < GGML_MAX_SRC; s++) {
732+
if (!n->src[s]) continue;
733+
const uint64_t sid = (uint64_t) (uintptr_t) roofline_storage(n->src[s]);
734+
if (!internal.count(sid) && seen.insert(sid).second) {
735+
topo.in_storage_ids.push_back(sid);
736+
topo.in_operands.push_back(make_src_operand(n->src[s]));
737+
}
738+
}
739+
}
740+
635741
// Re-point the current invocation at the fused record. The provisional head-only record
636742
// from begin_op stays in g_records; if no non-fused invocation references it, it is
637743
// simply never emitted (the report iterates g_invocations).
638744
std::lock_guard<std::mutex> lock(g_mutex);
639745
g_invocations[g_current_invocation] = geometry_id;
640746
if (g_records.find(geometry_id) == g_records.end()) g_records.emplace(geometry_id, std::move(rec));
747+
g_invocation_topology[g_current_invocation] = std::move(topo);
641748
}

0 commit comments

Comments
 (0)