8989#include < cstdio>
9090#include < cstdlib>
9191#include < string>
92+ #include < unordered_set>
9293#include < vector>
9394
9495static_assert (sizeof (half) == sizeof (ggml_fp16_t ), " wrong fp16 size" );
@@ -648,10 +649,15 @@ ggml_backend_cuda_context::~ggml_backend_cuda_context() {
648649 CUDA_CHECK (cudaStreamDestroy (streams[i][j]));
649650 }
650651 }
651- if (cublas_handles[i] != nullptr ) {
652- CUBLAS_CHECK (cublasDestroy (cublas_handles[i]));
652+ for (int j = 0 ; j < GGML_CUDA_MAX_STREAMS ; ++j) {
653+ if (cublas_handles[i][j] != nullptr ) {
654+ CUBLAS_CHECK (cublasDestroy (cublas_handles[i][j]));
655+ }
653656 }
654657 }
658+ if (concurrent_scratch != nullptr ) {
659+ ggml_backend_buffer_free (concurrent_scratch);
660+ }
655661}
656662
657663
@@ -3895,8 +3901,11 @@ static void ggml_cuda_graph_evaluate_and_capture(ggml_backend_cuda_context * cud
38953901 is_concurrent_event_active = false ;
38963902 concurrent_event = nullptr ;
38973903 } else {
3898- GGML_ASSERT (concurrent_event->stream_mapping .find (node) != concurrent_event->stream_mapping .end ());
3899- cuda_ctx->curr_stream_no = concurrent_event->stream_mapping [node];
3904+ // region nodes not mapped to a concurrent stream run on the main stream:
3905+ // this keeps the routed branch on the main stream while only the shared
3906+ // expert forks off (the vLLM shared-expert model)
3907+ auto it = concurrent_event->stream_mapping .find (node);
3908+ cuda_ctx->curr_stream_no = it != concurrent_event->stream_mapping .end () ? it->second : 0 ;
39003909 GGML_LOG_DEBUG (" Setting stream no to %d for node %s\n " , cuda_ctx->curr_stream_no , node->name );
39013910 }
39023911 } else if (i - prev_i > 1 ) {
@@ -3905,7 +3914,8 @@ static void ggml_cuda_graph_evaluate_and_capture(ggml_backend_cuda_context * cud
39053914 try_launch_concurrent_event (prev_node);
39063915
39073916 if (is_concurrent_event_active) {
3908- cuda_ctx->curr_stream_no = concurrent_event->stream_mapping [node];
3917+ auto it = concurrent_event->stream_mapping .find (node);
3918+ cuda_ctx->curr_stream_no = it != concurrent_event->stream_mapping .end () ? it->second : 0 ;
39093919 GGML_LOG_DEBUG (" Setting stream no to %d for node %s\n " , cuda_ctx->curr_stream_no , node->name );
39103920 }
39113921 }
@@ -4186,6 +4196,11 @@ static void ggml_backend_cuda_graph_optimize(ggml_backend_t backend, ggml_cgraph
41864196 // store {fork_idx, join_idx}
41874197 std::vector<std::pair<int , int >> concurrent_node_ranges;
41884198
4199+ // per-event lists of concurrent branch nodes to place in the dedicated scratch buffer (below),
4200+ // so the branches are mutually disjoint and disjoint from tensors read across the region -
4201+ // this replaces the fragile node interleaving that ggml-alloc/execution order can desync
4202+ std::vector<std::vector<const ggml_tensor *>> concurrent_groups;
4203+
41894204 for (const auto & [root_node, count] : fan_out) {
41904205 if (count >= min_fan_out && count <= max_fan_out) {
41914206 const int root_node_idx = node_indices[root_node];
@@ -4277,10 +4292,6 @@ static void ggml_backend_cuda_graph_optimize(ggml_backend_t backend, ggml_cgraph
42774292 int fork_node_idx = node_indices[root_node];
42784293 int join_node_idx = node_indices[join_node];
42794294
4280- int current_branch_idx = 0 ;
4281- int current_node_idx = fork_node_idx + 1 ;
4282- const int n_branches = nodes_per_branch.size ();
4283-
42844295 int total_branch_nodes = 0 ;
42854296 for (std::vector<const ggml_tensor *> branch_nodes : nodes_per_branch) {
42864297 total_branch_nodes += branch_nodes.size ();
@@ -4309,37 +4320,182 @@ static void ggml_backend_cuda_graph_optimize(ggml_backend_t backend, ggml_cgraph
43094320 GGML_LOG_DEBUG (" Adding stream at node %s %p\n " , root_node->name , root_node);
43104321 concurrent_node_ranges.emplace_back (fork_node_idx, join_node_idx);
43114322
4312- // interleave tensors to extend lifetimes so that ggml graph doesn't recycle them
4313- // example transformation:
4314- // [attn-norm, QMul, QNorm, QRope, KMul, KNorm, KRope, VMul, attn] ->
4315- // [attn-norm, QMul, KMul, VMul, QNorm, VNorm, QRope, KRope, attn]
4316- while (current_node_idx < join_node_idx) {
4317- std::vector<const ggml_tensor *> & branch_nodes = nodes_per_branch[current_branch_idx];
4318-
4319- bool has_node = false ;
4320- for (std::vector<const ggml_tensor *> branch_node : nodes_per_branch) {
4321- has_node |= branch_node.size () > 0 ;
4323+ // place all branch nodes in the dedicated scratch buffer (below) instead of
4324+ // interleaving them: ggml-alloc then keeps the branches mutually disjoint and
4325+ // disjoint from the fork output that every branch reads concurrently
4326+ std::vector<const ggml_tensor *> group;
4327+ for (const auto & branch_nodes : nodes_per_branch) {
4328+ for (const ggml_tensor * n : branch_nodes) {
4329+ group.push_back (n);
43224330 }
4331+ }
4332+ concurrent_groups.push_back (std::move (group));
4333+ }
4334+ }
4335+ }
43234336
4324- GGML_ASSERT (has_node);
4337+ // MoE shared-expert overlap: run the shared expert on a separate stream, overlapped with the
4338+ // routed experts. fork = the FFN-input norm feeding both branches, join = ggml_add(ffn_moe_out,
4339+ // ffn_shexp*). Operands are matched by the names set via cb() in the model graph. Decode only
4340+ // (gated below): prefill is compute-bound and gains nothing from the overlap.
4341+ const auto reach_backward = [](const ggml_tensor * start) {
4342+ std::unordered_set<const ggml_tensor *> seen;
4343+ std::vector<const ggml_tensor *> stack = { start };
4344+ while (!stack.empty ()) {
4345+ const ggml_tensor * t = stack.back ();
4346+ stack.pop_back ();
4347+ if (!t || seen.count (t)) {
4348+ continue ;
4349+ }
4350+ seen.insert (t);
4351+ for (int s = 0 ; s < GGML_MAX_SRC ; ++s) {
4352+ if (t->src [s]) {
4353+ stack.push_back (t->src [s]);
4354+ }
4355+ }
4356+ }
4357+ return seen;
4358+ };
43254359
4326- if (branch_nodes.empty ()) {
4327- current_branch_idx = (current_branch_idx + 1 ) % n_branches;
4328- continue ;
4329- }
4360+ for (int join_idx = 0 ; join_idx < cgraph->n_nodes ; ++join_idx) {
4361+ ggml_tensor * join_node = cgraph->nodes [join_idx];
4362+ if (join_node->op != GGML_OP_ADD ) {
4363+ continue ;
4364+ }
43304365
4331- cgraph->nodes [current_node_idx] = const_cast <ggml_tensor *>(branch_nodes.front ());
4332- current_node_idx++;
4333- branch_nodes.erase (branch_nodes.begin ());
4366+ // Only overlap during decode (single token). Overlapping the shared expert only helps when
4367+ // the routed branch leaves the GPU underutilized for it to run alongside; that is the case in
4368+ // decode (batch 1, latency/occupancy-bound) but not in prefill, where the routed matmuls are
4369+ // large and already saturate the GPU, so the overlap adds contention without a speedup.
4370+ if (ggml_nrows (join_node) > 1 ) {
4371+ continue ;
4372+ }
43344373
4335- // append all empty nodes
4336- while (!branch_nodes.empty () && is_noop (branch_nodes.front ())) {
4337- cgraph->nodes [current_node_idx] = const_cast <ggml_tensor *>(branch_nodes.front ());
4338- current_node_idx++;
4339- branch_nodes.erase (branch_nodes.begin ());
4340- }
4374+ ggml_tensor * routed_out = nullptr ;
4375+ ggml_tensor * shexp_out = nullptr ;
4376+ for (int s = 0 ; s < 2 ; ++s) {
4377+ ggml_tensor * x = join_node->src [s];
4378+ ggml_tensor * y = join_node->src [1 - s];
4379+ if (x && y && strstr (x->name , " ffn_moe_out" ) && strstr (y->name , " ffn_shexp" )) {
4380+ routed_out = x;
4381+ shexp_out = y;
4382+ }
4383+ }
4384+ if (!routed_out || !shexp_out) {
4385+ continue ;
4386+ }
4387+
4388+ const std::unordered_set<const ggml_tensor *> reach_routed = reach_backward (routed_out);
4389+ const std::unordered_set<const ggml_tensor *> reach_shexp = reach_backward (shexp_out);
43414390
4342- current_branch_idx = (current_branch_idx + 1 ) % n_branches;
4391+ // fork = highest-index node reachable from both branches (the ffn_norm output)
4392+ int fork_idx = -1 ;
4393+ for (const ggml_tensor * t : reach_routed) {
4394+ if (!reach_shexp.count (t)) {
4395+ continue ;
4396+ }
4397+ auto it = node_indices.find (t);
4398+ if (it != node_indices.end () && it->second < join_idx && it->second > fork_idx) {
4399+ fork_idx = it->second ;
4400+ }
4401+ }
4402+ if (fork_idx < 0 ) {
4403+ continue ;
4404+ }
4405+
4406+ bool overlaps = false ;
4407+ for (const auto & [start, end] : concurrent_node_ranges) {
4408+ if (!(join_idx < start || fork_idx > end)) {
4409+ overlaps = true ;
4410+ }
4411+ }
4412+ if (overlaps) {
4413+ continue ;
4414+ }
4415+
4416+ // partition the region (fork_idx, join_idx): shared-expert nodes -> stream 2, routed -> 1
4417+ std::vector<std::vector<const ggml_tensor *>> nodes_per_branch (2 );
4418+ for (int i = fork_idx + 1 ; i < join_idx; ++i) {
4419+ const ggml_tensor * n = cgraph->nodes [i];
4420+ const int branch = reach_shexp.count (n) ? 1 : 0 ;
4421+ nodes_per_branch[branch].push_back (n);
4422+ }
4423+ if (nodes_per_branch[0 ].empty () || nodes_per_branch[1 ].empty ()) {
4424+ continue ;
4425+ }
4426+
4427+ // vLLM shared-expert model: the routed experts stay on the main stream and only the shared
4428+ // expert forks onto a single aux stream, joined at the add. Keeping the large routed branch
4429+ // on the main stream avoids migrating it and needs only one fork/join.
4430+ ggml_cuda_concurrent_event concurrent_event (1 );
4431+ concurrent_event.join_node = join_node;
4432+ for (const ggml_tensor * n : nodes_per_branch[1 ]) {
4433+ concurrent_event.stream_mapping [n] = 1 ;
4434+ }
4435+
4436+ const ggml_tensor * fork_node = cgraph->nodes [fork_idx];
4437+ concurrent_event.original_order .reserve (join_idx - fork_idx - 1 );
4438+ for (int i = fork_idx + 1 ; i < join_idx; ++i) {
4439+ concurrent_event.original_order .push_back (cgraph->nodes [i]);
4440+ }
4441+
4442+ std::unordered_map<const ggml_tensor *, ggml_cuda_concurrent_event> & concurrent_events = cuda_ctx->stream_context ().concurrent_events ;
4443+ if (concurrent_events.find (fork_node) != concurrent_events.end ()) {
4444+ continue ;
4445+ }
4446+ concurrent_events.emplace (fork_node, std::move (concurrent_event));
4447+ GGML_LOG_DEBUG (" Adding shared-expert stream at node %s %p\n " , fork_node->name , fork_node);
4448+ concurrent_node_ranges.emplace_back (fork_idx, join_idx);
4449+
4450+ // the shared-expert nodes get a dedicated buffer (below), so the graph order is left intact
4451+ // and no interleaving is needed to keep the branch non-overlapping
4452+ concurrent_groups.push_back (nodes_per_branch[1 ]);
4453+ }
4454+
4455+ // Place every concurrent branch (attention QKV and MoE shared-expert) in a dedicated buffer so
4456+ // its nodes never share an address with each other or with tensors read across the region (which
4457+ // ggml-alloc could otherwise recycle, corrupting concurrent reads). Layers run sequentially, so
4458+ // one buffer sized to the largest region is reused across all of them; within a region each node
4459+ // gets a distinct offset so the concurrent scratch stays disjoint.
4460+ if (!concurrent_groups.empty ()) {
4461+ const size_t alignment = 128 ;
4462+
4463+ const auto group_footprint = [&](const std::vector<const ggml_tensor *> & group) {
4464+ size_t off = 0 ;
4465+ for (const ggml_tensor * n : group) {
4466+ if (is_noop (n) || n->view_src != nullptr ) {
4467+ continue ;
4468+ }
4469+ off += GGML_PAD (ggml_nbytes (n), alignment);
4470+ }
4471+ return off;
4472+ };
4473+
4474+ size_t needed = 0 ;
4475+ for (const auto & group : concurrent_groups) {
4476+ needed = std::max (needed, group_footprint (group));
4477+ }
4478+
4479+ if (needed > 0 ) {
4480+ if (cuda_ctx->concurrent_scratch == nullptr || cuda_ctx->concurrent_scratch_size < needed) {
4481+ if (cuda_ctx->concurrent_scratch != nullptr ) {
4482+ ggml_backend_buffer_free (cuda_ctx->concurrent_scratch );
4483+ }
4484+ cuda_ctx->concurrent_scratch = ggml_backend_buft_alloc_buffer (ggml_backend_cuda_buffer_type (cuda_ctx->device ), needed);
4485+ cuda_ctx->concurrent_scratch_size = needed;
4486+ }
4487+
4488+ char * const base = (char *) ggml_backend_buffer_get_base (cuda_ctx->concurrent_scratch );
4489+ for (const auto & group : concurrent_groups) {
4490+ size_t off = 0 ;
4491+ for (const ggml_tensor * cn : group) {
4492+ if (is_noop (cn) || cn->view_src != nullptr ) {
4493+ continue ;
4494+ }
4495+ ggml_tensor * n = const_cast <ggml_tensor *>(cn);
4496+ n->data = base + off;
4497+ n->buffer = cuda_ctx->concurrent_scratch ;
4498+ off += GGML_PAD (ggml_nbytes (n), alignment);
43434499 }
43444500 }
43454501 }
0 commit comments