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+
83105std::mutex g_mutex;
84106std::unordered_map<uint64_t , op_record> g_records; // geometry id -> geometry
85107std::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).
94116std::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)
95118std::atomic<uint64_t > g_next_invocation{1 };
96119thread_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.
184219void 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