8585#include < cstdio>
8686#include < cstdlib>
8787#include < string>
88+ #include < unordered_set>
8889#include < vector>
8990
9091static_assert (sizeof (half) == sizeof (ggml_fp16_t ), " wrong fp16 size" );
@@ -4358,8 +4359,11 @@ static void ggml_cuda_graph_evaluate_and_capture(ggml_backend_cuda_context * cud
43584359 is_concurrent_event_active = false ;
43594360 concurrent_event = nullptr ;
43604361 } else {
4361- GGML_ASSERT (concurrent_event->stream_mapping .find (node) != concurrent_event->stream_mapping .end ());
4362- cuda_ctx->curr_stream_no = concurrent_event->stream_mapping [node];
4362+ // region nodes not mapped to a concurrent stream run on the main stream:
4363+ // this keeps the routed branch on the main stream while only the shared
4364+ // expert forks off
4365+ auto it = concurrent_event->stream_mapping .find (node);
4366+ cuda_ctx->curr_stream_no = it != concurrent_event->stream_mapping .end () ? it->second : 0 ;
43634367 GGML_LOG_DEBUG (" Setting stream no to %d for node %s\n " , cuda_ctx->curr_stream_no , node->name );
43644368 }
43654369 } else if (i - prev_i > 1 ) {
@@ -4368,7 +4372,8 @@ static void ggml_cuda_graph_evaluate_and_capture(ggml_backend_cuda_context * cud
43684372 try_launch_concurrent_event (prev_node);
43694373
43704374 if (is_concurrent_event_active) {
4371- cuda_ctx->curr_stream_no = concurrent_event->stream_mapping [node];
4375+ auto it = concurrent_event->stream_mapping .find (node);
4376+ cuda_ctx->curr_stream_no = it != concurrent_event->stream_mapping .end () ? it->second : 0 ;
43724377 GGML_LOG_DEBUG (" Setting stream no to %d for node %s\n " , cuda_ctx->curr_stream_no , node->name );
43734378 }
43744379 }
@@ -4558,6 +4563,132 @@ static void ggml_backend_cuda_event_wait(ggml_backend_t backend, ggml_backend_ev
45584563 }
45594564}
45604565
4566+ // MoE shared-expert overlap: run the shared expert on a separate stream, overlapped with the
4567+ // routed experts. fork = the FFN-input norm feeding both branches, join = ggml_add(ffn_moe_out,
4568+ // ffn_shexp*). Operands are matched by the names set via cb() in the model graph. Decode only
4569+ // (gated below): prefill is compute-bound and gains nothing from the overlap.
4570+ static void ggml_cuda_detect_shared_expert_concurrency (
4571+ ggml_cgraph * cgraph,
4572+ ggml_backend_cuda_context * cuda_ctx,
4573+ const std::unordered_map<const ggml_tensor *, int > & node_indices,
4574+ std::vector<std::pair<int , int >> & concurrent_node_ranges,
4575+ std::vector<std::vector<const ggml_tensor *>> & concurrent_groups) {
4576+ const auto reach_backward = [](const ggml_tensor * start) {
4577+ std::unordered_set<const ggml_tensor *> seen;
4578+ std::vector<const ggml_tensor *> stack = { start };
4579+ while (!stack.empty ()) {
4580+ const ggml_tensor * t = stack.back ();
4581+ stack.pop_back ();
4582+ if (!t || seen.count (t)) {
4583+ continue ;
4584+ }
4585+ seen.insert (t);
4586+ for (int s = 0 ; s < GGML_MAX_SRC ; ++s) {
4587+ if (t->src [s]) {
4588+ stack.push_back (t->src [s]);
4589+ }
4590+ }
4591+ }
4592+ return seen;
4593+ };
4594+
4595+ for (int join_idx = 0 ; join_idx < cgraph->n_nodes ; ++join_idx) {
4596+ ggml_tensor * join_node = cgraph->nodes [join_idx];
4597+ if (join_node->op != GGML_OP_ADD ) {
4598+ continue ;
4599+ }
4600+
4601+ // Only overlap in the mat-vec regime (up to MMVQ_MAX_BATCH_SIZE tokens). The shared-expert
4602+ // overlap only helps when the routed branch leaves the GPU underutilized for it to run
4603+ // alongside; that holds while the matmuls stay in the memory/occupancy-bound mat-vec path,
4604+ // but not once the token count grows past it and the routed matmuls saturate the GPU
4605+ // (prefill), where the overlap only adds contention.
4606+ if (ggml_nrows (join_node) > MMVQ_MAX_BATCH_SIZE ) {
4607+ continue ;
4608+ }
4609+
4610+ ggml_tensor * routed_out = nullptr ;
4611+ ggml_tensor * shexp_out = nullptr ;
4612+ for (int s = 0 ; s < 2 ; ++s) {
4613+ ggml_tensor * x = join_node->src [s];
4614+ ggml_tensor * y = join_node->src [1 - s];
4615+ if (x && y && strstr (x->name , " ffn_moe_out" ) && strstr (y->name , " ffn_shexp" )) {
4616+ routed_out = x;
4617+ shexp_out = y;
4618+ }
4619+ }
4620+ if (!routed_out || !shexp_out) {
4621+ continue ;
4622+ }
4623+
4624+ const std::unordered_set<const ggml_tensor *> reach_routed = reach_backward (routed_out);
4625+ const std::unordered_set<const ggml_tensor *> reach_shexp = reach_backward (shexp_out);
4626+
4627+ // fork = highest-index node reachable from both branches (the ffn_norm output)
4628+ int fork_idx = -1 ;
4629+ for (const ggml_tensor * t : reach_routed) {
4630+ if (!reach_shexp.count (t)) {
4631+ continue ;
4632+ }
4633+ auto it = node_indices.find (t);
4634+ if (it != node_indices.end () && it->second < join_idx && it->second > fork_idx) {
4635+ fork_idx = it->second ;
4636+ }
4637+ }
4638+ if (fork_idx < 0 ) {
4639+ continue ;
4640+ }
4641+
4642+ bool overlaps = false ;
4643+ for (const auto & [start, end] : concurrent_node_ranges) {
4644+ if (!(join_idx < start || fork_idx > end)) {
4645+ overlaps = true ;
4646+ }
4647+ }
4648+ if (overlaps) {
4649+ continue ;
4650+ }
4651+
4652+ // partition the region (fork_idx, join_idx): shared-expert nodes -> stream 2, routed -> 1
4653+ std::vector<std::vector<const ggml_tensor *>> nodes_per_branch (2 );
4654+ for (int i = fork_idx + 1 ; i < join_idx; ++i) {
4655+ const ggml_tensor * n = cgraph->nodes [i];
4656+ const int branch = reach_shexp.count (n) ? 1 : 0 ;
4657+ nodes_per_branch[branch].push_back (n);
4658+ }
4659+ if (nodes_per_branch[0 ].empty () || nodes_per_branch[1 ].empty ()) {
4660+ continue ;
4661+ }
4662+
4663+ // the routed experts stay on the main stream and only the shared expert forks onto a single
4664+ // aux stream, joined at the add. Keeping the large routed branch on the main stream avoids
4665+ // migrating it and needs only one fork/join.
4666+ ggml_cuda_concurrent_event concurrent_event (1 );
4667+ concurrent_event.join_node = join_node;
4668+ for (const ggml_tensor * n : nodes_per_branch[1 ]) {
4669+ concurrent_event.stream_mapping [n] = 1 ;
4670+ }
4671+
4672+ const ggml_tensor * fork_node = cgraph->nodes [fork_idx];
4673+ concurrent_event.original_order .reserve (join_idx - fork_idx - 1 );
4674+ for (int i = fork_idx + 1 ; i < join_idx; ++i) {
4675+ concurrent_event.original_order .push_back (cgraph->nodes [i]);
4676+ }
4677+
4678+ std::unordered_map<const ggml_tensor *, ggml_cuda_concurrent_event> & concurrent_events = cuda_ctx->stream_context ().concurrent_events ;
4679+ if (concurrent_events.find (fork_node) != concurrent_events.end ()) {
4680+ continue ;
4681+ }
4682+ concurrent_events.emplace (fork_node, std::move (concurrent_event));
4683+ GGML_LOG_DEBUG (" Adding shared-expert stream at node %s %p\n " , fork_node->name , fork_node);
4684+ concurrent_node_ranges.emplace_back (fork_idx, join_idx);
4685+
4686+ // the shared-expert nodes get a dedicated buffer (below), so the graph order is left intact
4687+ // and no interleaving is needed to keep the branch non-overlapping
4688+ concurrent_groups.push_back (nodes_per_branch[1 ]);
4689+ }
4690+ }
4691+
45614692static void ggml_backend_cuda_graph_optimize (ggml_backend_t backend, ggml_cgraph * cgraph) {
45624693 ggml_backend_cuda_context * cuda_ctx = (ggml_backend_cuda_context *) backend->context ;
45634694
@@ -4780,11 +4911,13 @@ static void ggml_backend_cuda_graph_optimize(ggml_backend_t backend, ggml_cgraph
47804911 }
47814912 }
47824913
4783- // Place every concurrent branch in a dedicated buffer so its nodes never share an address with
4784- // each other or with tensors read across the region (which ggml-alloc could otherwise recycle,
4785- // corrupting concurrent reads). Layers run sequentially, so one buffer sized to the largest
4786- // region is reused across all of them; within a region each node gets a distinct offset so the
4787- // concurrent scratch stays disjoint.
4914+ ggml_cuda_detect_shared_expert_concurrency (cgraph, cuda_ctx, node_indices, concurrent_node_ranges, concurrent_groups);
4915+
4916+ // Place every concurrent branch (attention QKV and MoE shared-expert) in a dedicated buffer so
4917+ // its nodes never share an address with each other or with tensors read across the region (which
4918+ // ggml-alloc could otherwise recycle, corrupting concurrent reads). Layers run sequentially, so
4919+ // one buffer sized to the largest region is reused across all of them; within a region each node
4920+ // gets a distinct offset so the concurrent scratch stays disjoint.
47884921 if (!concurrent_groups.empty ()) {
47894922 const size_t alignment = 128 ;
47904923
0 commit comments