Skip to content

Commit fca6b93

Browse files
committed
Harden DFlash CUDA multi-GPU fallbacks
1 parent fdfd79e commit fca6b93

6 files changed

Lines changed: 181 additions & 80 deletions

File tree

common/speculative.cpp

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -77,16 +77,22 @@ static bool common_dflash_kv_cache_disabled() {
7777
return disabled;
7878
}
7979

80-
static bool common_dflash_gpu_ring_allowed(llama_context * ctx_tgt) {
81-
const llama_model * model_tgt = ctx_tgt ? llama_get_model(ctx_tgt) : nullptr;
82-
if (!model_tgt) {
83-
return true;
80+
static bool common_dflash_gpu_ring_env_enabled() {
81+
const char * env = std::getenv("GGML_DFLASH_GPU_RING");
82+
return env == nullptr || std::atoi(env) != 0;
83+
}
84+
85+
static bool common_dflash_gpu_ring_allowed(llama_context * ctx_tgt, llama_context * ctx_dft) {
86+
if (!common_dflash_gpu_ring_env_enabled()) {
87+
LOG_INF("dflash: GPU cross ring disabled by GGML_DFLASH_GPU_RING=0; using CPU hidden capture\n");
88+
return false;
8489
}
8590

86-
const int32_t n_devices = llama_model_n_devices(model_tgt);
87-
if (n_devices > 1) {
88-
LOG_INF("dflash: multi-GPU target detected (%d devices); disabling GPU cross ring because target hidden capture uses CPU fallback\n",
89-
n_devices);
91+
const int32_t n_tgt_devices = ctx_tgt ? llama_model_n_devices(llama_get_model(ctx_tgt)) : 1;
92+
const int32_t n_dft_devices = ctx_dft ? llama_model_n_devices(llama_get_model(ctx_dft)) : 1;
93+
if (n_tgt_devices > 1 || n_dft_devices > 1) {
94+
LOG_INF("dflash: multi-GPU placement detected (target=%d devices, drafter=%d devices); disabling GPU cross ring and graph hidden capture\n",
95+
n_tgt_devices, n_dft_devices);
9096
return false;
9197
}
9298

@@ -1621,13 +1627,20 @@ struct common_speculative_state_dflash : public common_speculative_state {
16211627

16221628
batch_dft = llama_batch_init(block_size, 0, 1);
16231629

1624-
// try to allocate GPU ring buffer on drafter's GPU
1625-
if (common_dflash_gpu_ring_allowed(ctx_tgt)) {
1630+
// Try to allocate the GPU ring only when both contexts can safely share
1631+
// single-backend cross tensors. Otherwise keep the target eval callback
1632+
// active so the CPU ring fallback has hidden states to consume.
1633+
const bool gpu_ring_requested = common_dflash_gpu_ring_allowed(ctx_tgt, ctx_dft);
1634+
llama_set_dflash_gpu_capture(ctx_tgt, gpu_ring_requested);
1635+
if (gpu_ring_requested) {
16261636
gpu_ring_handle = llama_dflash_cross_ring_gpu_init(ctx_dft, n_target_layers, n_embd, cross_ctx);
1627-
if (gpu_ring_handle) {
1628-
LOG_INF("dflash: GPU cross ring enabled (%d layers x %d slots x %d embd)\n",
1629-
n_target_layers, cross_ctx, n_embd);
1630-
}
1637+
}
1638+
if (gpu_ring_handle) {
1639+
LOG_INF("dflash: GPU cross ring enabled (%d layers x %d slots x %d embd)\n",
1640+
n_target_layers, cross_ctx, n_embd);
1641+
} else if (gpu_ring_requested) {
1642+
llama_set_dflash_gpu_capture(ctx_tgt, false);
1643+
LOG_WRN("dflash: GPU cross ring unavailable; using CPU hidden capture\n");
16311644
}
16321645

16331646
LOG_INF("dflash: block_size=%d, mask_token=%d, n_target_layers=%d, n_embd=%d\n",

ggml/src/ggml-cuda/ggml-cuda.cu

Lines changed: 90 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1389,6 +1389,12 @@ ggml_backend_buffer_type_t ggml_backend_cuda_host_buffer_type() {
13891389
return &ggml_backend_cuda_buffer_type_host;
13901390
}
13911391

1392+
static bool ggml_cuda_buffer_visible_to_backend(ggml_backend_cuda_context * cuda_ctx, ggml_backend_buffer_type_t buft, bool integrated) {
1393+
return buft == ggml_backend_cuda_buffer_type(cuda_ctx->device) ||
1394+
ggml_backend_buft_is_cuda_split(buft) ||
1395+
(integrated && ggml_backend_buft_is_cuda_host(buft));
1396+
}
1397+
13921398
//static bool ggml_backend_buffer_is_cuda_host(ggml_backend_buffer_t buffer) {
13931399
// return buffer->buft->iface.get_name == ggml_backend_cuda_host_buffer_type_name;
13941400
//}
@@ -3101,54 +3107,87 @@ static void ggml_backend_cuda_synchronize(ggml_backend_t backend) {
31013107
GGML_UNUSED(backend);
31023108
}
31033109

3104-
static bool ggml_cuda_buffer_visible_to_backend(
3105-
ggml_backend_cuda_context * cuda_ctx,
3106-
ggml_backend_buffer_type_t buft,
3107-
bool integrated) {
3108-
return buft == ggml_backend_cuda_buffer_type(cuda_ctx->device) ||
3109-
ggml_backend_buft_is_cuda_split(buft) ||
3110-
(integrated && ggml_backend_buft_is_cuda_host(buft));
3111-
}
3112-
3113-
#ifndef NDEBUG
31143110
static void ggml_cuda_log_nonlocal_src_buffer(
31153111
ggml_backend_cuda_context * cuda_ctx,
31163112
const ggml_tensor * node,
3117-
int src_idx) {
3118-
const ggml_tensor * src = node->src[src_idx];
3113+
int src_index,
3114+
bool integrated,
3115+
const char * action) {
3116+
const ggml_tensor * src = src_index >= 0 ? node->src[src_index] : node;
3117+
const void * data = src ? src->data : nullptr;
31193118
int ptr_device = -1;
3120-
int ptr_type = -1;
3119+
const char * ptr_type = "unknown";
31213120

3122-
if (src && src->data) {
3121+
if (data) {
31233122
cudaPointerAttributes attr;
3124-
cudaError_t err = cudaPointerGetAttributes(&attr, src->data);
3123+
cudaError_t err = cudaPointerGetAttributes(&attr, data);
31253124
if (err == cudaSuccess) {
31263125
ptr_device = attr.device;
31273126
#if CUDART_VERSION >= 10000
3128-
ptr_type = attr.type;
3127+
switch (attr.type) {
31293128
#else
3130-
ptr_type = attr.memoryType;
3129+
switch (attr.memoryType) {
31313130
#endif
3131+
case cudaMemoryTypeHost: ptr_type = "host"; break;
3132+
case cudaMemoryTypeDevice: ptr_type = "device"; break;
3133+
case cudaMemoryTypeManaged: ptr_type = "managed"; break;
3134+
default: ptr_type = "other"; break;
3135+
}
31323136
} else {
3133-
(void) cudaGetLastError();
3137+
cudaGetLastError();
31343138
}
31353139
}
31363140

31373141
GGML_LOG_ERROR(
3138-
"%s: source buffer is not visible to CUDA backend device %d: node=%s op=%s src[%d]=%s src_buffer=%s src_ptr=%p ptr_device=%d ptr_type=%d dst_buffer=%s\n",
3139-
__func__,
3140-
cuda_ctx->device,
3141-
node->name,
3142-
ggml_op_name(node->op),
3143-
src_idx,
3144-
src ? src->name : "(null)",
3145-
src && src->buffer ? ggml_backend_buft_name(src->buffer->buft) : "(no buffer)",
3146-
src ? src->data : nullptr,
3147-
ptr_device,
3148-
ptr_type,
3149-
ggml_backend_buft_name(ggml_backend_cuda_buffer_type(cuda_ctx->device)));
3142+
"%s: source buffer is not visible to CUDA backend device; action=%s backend_device=%d integrated=%d node=%s op=%s src[%d]=%s src_buft=%s dst_buft=%s src_data=%p ptr_type=%s ptr_device=%d\n",
3143+
__func__,
3144+
action ? action : "fail",
3145+
cuda_ctx->device,
3146+
integrated ? 1 : 0,
3147+
node ? node->name : "(null)",
3148+
node ? ggml_op_name(node->op) : "(null)",
3149+
src_index,
3150+
src ? src->name : "(null)",
3151+
src && src->buffer ? ggml_backend_buft_name(src->buffer->buft) : "(none)",
3152+
node && node->buffer ? ggml_backend_buft_name(node->buffer->buft) : "(none)",
3153+
data,
3154+
ptr_type,
3155+
ptr_device);
3156+
}
3157+
3158+
static bool ggml_cuda_graph_node_buffers_visible(
3159+
ggml_backend_cuda_context * cuda_ctx,
3160+
const ggml_tensor * node,
3161+
bool integrated,
3162+
bool log_errors) {
3163+
if (!node || ggml_is_empty(node) || node->op == GGML_OP_RESHAPE || node->op == GGML_OP_TRANSPOSE ||
3164+
node->op == GGML_OP_VIEW || node->op == GGML_OP_PERMUTE || node->op == GGML_OP_NONE ||
3165+
(node->flags & GGML_TENSOR_FLAG_COMPUTE) == 0) {
3166+
return true;
3167+
}
3168+
3169+
if (!node->buffer || node->buffer->buft != ggml_backend_cuda_buffer_type(cuda_ctx->device)) {
3170+
if (log_errors) {
3171+
ggml_cuda_log_nonlocal_src_buffer(cuda_ctx, node, -1, integrated, "fail graph compute");
3172+
}
3173+
return false;
3174+
}
3175+
3176+
for (int j = 0; j < GGML_MAX_SRC; ++j) {
3177+
const ggml_tensor * src = node->src[j];
3178+
if (!src) {
3179+
continue;
3180+
}
3181+
if (!src->buffer || !ggml_cuda_buffer_visible_to_backend(cuda_ctx, src->buffer->buft, integrated)) {
3182+
if (log_errors) {
3183+
ggml_cuda_log_nonlocal_src_buffer(cuda_ctx, node, j, integrated, "fail graph compute");
3184+
}
3185+
return false;
3186+
}
3187+
}
3188+
3189+
return true;
31503190
}
3151-
#endif
31523191

31533192
#ifdef USE_CUDA_GRAPH
31543193
static bool ggml_cuda_graph_check_compability(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph * cgraph) {
@@ -3169,7 +3208,6 @@ static bool ggml_cuda_graph_check_compability(ggml_backend_cuda_context * cuda_c
31693208
if (!src || !src->buffer) {
31703209
continue;
31713210
}
3172-
31733211
const ggml_backend_buffer_type_t src_buft = src->buffer->buft;
31743212
if (ggml_backend_buft_is_cuda_split(src_buft)) {
31753213
use_cuda_graph = false; // Split buffers are not supported by CUDA graph capture
@@ -3189,6 +3227,9 @@ static bool ggml_cuda_graph_check_compability(ggml_backend_cuda_context * cuda_c
31893227
break;
31903228
}
31913229
}
3230+
if (!use_cuda_graph) {
3231+
break;
3232+
}
31923233

31933234
// [TAG_MUL_MAT_ID_CUDA_GRAPHS]
31943235
if (node->op == GGML_OP_MUL_MAT_ID) {
@@ -4128,7 +4169,7 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph
41284169
}
41294170

41304171

4131-
static void ggml_cuda_graph_evaluate_and_capture(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph * cgraph, const bool use_cuda_graph, const bool cuda_graph_update_required, const void * graph_key) {
4172+
static enum ggml_status ggml_cuda_graph_evaluate_and_capture(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph * cgraph, const bool use_cuda_graph, const bool cuda_graph_update_required, const void * graph_key) {
41324173
bool graph_evaluated_or_captured = false;
41334174

41344175
// flag used to determine whether it is an integrated_gpu
@@ -4280,34 +4321,15 @@ static void ggml_cuda_graph_evaluate_and_capture(ggml_backend_cuda_context * cud
42804321
i += nodes_to_skip;
42814322
continue;
42824323
}
4283-
#ifndef NDEBUG
4284-
const ggml_backend_buffer_type_t local_buft = ggml_backend_cuda_buffer_type(cuda_ctx->device);
4285-
if (node->buffer->buft != local_buft) {
4286-
GGML_LOG_ERROR("%s: node buffer is not local to CUDA backend device %d: node=%s op=%s node_buffer=%s dst_buffer=%s\n",
4287-
__func__, cuda_ctx->device, node->name, ggml_op_name(node->op),
4288-
ggml_backend_buft_name(node->buffer->buft), ggml_backend_buft_name(local_buft));
4289-
}
4290-
assert(node->buffer->buft == local_buft);
4291-
for (int j = 0; j < GGML_MAX_SRC; j++) {
4292-
if (node->src[j] != nullptr) {
4293-
assert(node->src[j]->buffer);
4294-
const ggml_backend_buffer_type_t src_buft = node->src[j]->buffer->buft;
4295-
const bool src_visible = ggml_cuda_buffer_visible_to_backend(cuda_ctx, src_buft, integrated);
4296-
if (!src_visible) {
4297-
ggml_cuda_log_nonlocal_src_buffer(cuda_ctx, node, j);
4298-
}
4299-
assert(src_visible);
4300-
}
4324+
if (!ggml_cuda_graph_node_buffers_visible(cuda_ctx, node, integrated, true)) {
4325+
return GGML_STATUS_FAILED;
43014326
}
4302-
#else
4303-
GGML_UNUSED(integrated);
4304-
#endif // NDEBUG
43054327

43064328
bool ok = ggml_cuda_compute_forward(*cuda_ctx, node);
43074329
if (!ok) {
43084330
GGML_LOG_ERROR("%s: op not supported %s (%s)\n", __func__, node->name, ggml_op_name(node->op));
4331+
return GGML_STATUS_FAILED;
43094332
}
4310-
GGML_ASSERT(ok);
43114333

43124334
if (!is_concurrent_event_active) {
43134335
try_launch_concurrent_event(node);
@@ -4350,6 +4372,8 @@ static void ggml_cuda_graph_evaluate_and_capture(ggml_backend_cuda_context * cud
43504372
graph_evaluated_or_captured = true;
43514373
#endif // USE_CUDA_GRAPH
43524374
}
4375+
4376+
return GGML_STATUS_SUCCESS;
43534377
}
43544378

43554379
#ifdef USE_CUDA_GRAPH
@@ -4413,6 +4437,13 @@ static enum ggml_status ggml_backend_cuda_graph_compute(ggml_backend_t backend,
44134437
}
44144438
#endif // USE_CUDA_GRAPH
44154439

4440+
const bool integrated = ggml_cuda_info().devices[cuda_ctx->device].integrated;
4441+
for (int i = 0; i < cgraph->n_nodes; ++i) {
4442+
if (!ggml_cuda_graph_node_buffers_visible(cuda_ctx, cgraph->nodes[i], integrated, true)) {
4443+
return GGML_STATUS_FAILED;
4444+
}
4445+
}
4446+
44164447
if (use_cuda_graph && cuda_graph_update_required) {
44174448
// Start CUDA graph capture
44184449
{
@@ -4423,7 +4454,10 @@ static enum ggml_status ggml_backend_cuda_graph_compute(ggml_backend_t backend,
44234454
CUDA_CHECK(cudaStreamBeginCapture(cuda_ctx->stream(), cudaStreamCaptureModeRelaxed));
44244455
}
44254456

4426-
ggml_cuda_graph_evaluate_and_capture(cuda_ctx, cgraph, use_cuda_graph, cuda_graph_update_required, graph_key);
4457+
const ggml_status status = ggml_cuda_graph_evaluate_and_capture(cuda_ctx, cgraph, use_cuda_graph, cuda_graph_update_required, graph_key);
4458+
if (status != GGML_STATUS_SUCCESS) {
4459+
return status;
4460+
}
44274461

44284462
return GGML_STATUS_SUCCESS;
44294463
}

include/llama.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1088,6 +1088,11 @@ extern "C" {
10881088
// Pass n_layers=0 to disable capture
10891089
LLAMA_API void llama_set_dflash_capture(struct llama_context * ctx, const int32_t * layer_ids, int32_t n_layers);
10901090

1091+
// DFlash: enable graph-embedded GPU hidden/tape capture for target decode.
1092+
// Disable this before decode when the drafter cannot consume GPU cross-ring
1093+
// tensors directly, so the eval callback keeps CPU hidden buffers populated.
1094+
LLAMA_API void llama_set_dflash_gpu_capture(struct llama_context * ctx, bool enabled);
1095+
10911096
// DFlash: set drafter sampling temperature (Gumbel-max trick)
10921097
// temp=0: greedy argmax (default), temp>0: sample from softmax(logits/temp)
10931098
LLAMA_API void llama_set_dflash_sample_temp(struct llama_context * ctx, float temp);

src/llama-context.cpp

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1432,6 +1432,30 @@ void llama_context::set_dflash_capture(const int32_t * layer_ids, int32_t n_laye
14321432
}
14331433
}
14341434

1435+
void llama_context::set_dflash_gpu_capture(bool enabled) {
1436+
if (!dflash_capture) {
1437+
return;
1438+
}
1439+
1440+
dflash_capture->gpu_capture_enabled = enabled;
1441+
1442+
if (enabled) {
1443+
return;
1444+
}
1445+
1446+
dflash_capture->hidden_gpu.clear();
1447+
dflash_capture->tapes.clear();
1448+
cparams.tape_gpu = nullptr;
1449+
cparams.tape_gpu_n_seqs = 0;
1450+
cparams.hidden_gpu_n_seqs = 0;
1451+
for (int s = 0; s < (int) LLAMA_DFLASH_MAX_SLOTS; ++s) {
1452+
cparams.tape_gpu_seqs[s] = nullptr;
1453+
cparams.hidden_gpu_seqs[s] = nullptr;
1454+
}
1455+
cparams.cb_eval = dflash_eval_callback;
1456+
cparams.cb_eval_user_data = dflash_capture.get();
1457+
}
1458+
14351459
void llama_context::dflash_reset_hidden_capture() {
14361460
if (!dflash_capture) {
14371461
return;
@@ -1558,6 +1582,12 @@ void llama_context::allocate_tape_gpu(int n_slots, int max_tokens) {
15581582
// populate recurrent-layer metadata if the caller beat set_tape_recording() to it
15591583
dflash_ensure_recurrent_setup();
15601584

1585+
if (!dflash_capture->gpu_capture_enabled) {
1586+
dflash_capture->hidden_gpu.clear();
1587+
dflash_capture->tapes.clear();
1588+
return;
1589+
}
1590+
15611591
if (model.n_devices() > 1) {
15621592
dflash_capture->hidden_gpu.clear();
15631593
dflash_capture->tapes.clear();
@@ -1673,6 +1703,10 @@ void llama_context::allocate_hidden_gpu(int n_slots, int max_tokens) {
16731703
if (n_slots < 1) {
16741704
n_slots = 1;
16751705
}
1706+
if (!dflash_capture->gpu_capture_enabled) {
1707+
dflash_capture->hidden_gpu.clear();
1708+
return;
1709+
}
16761710
if (model.n_devices() > 1) {
16771711
dflash_capture->hidden_gpu.clear();
16781712
return;
@@ -4234,7 +4268,7 @@ int llama_context::decode(const llama_batch & batch_inp) {
42344268
// correct layer_hiddens slot. Populate per-seq tape pointers for the
42354269
// graph builder so GPU tape copies target the correct per-slot buffers.
42364270
if (dflash_capture) {
4237-
const bool dflash_gpu_capture_ready = model.n_devices() <= 1;
4271+
const bool dflash_gpu_capture_ready = model.n_devices() <= 1 && dflash_capture->gpu_capture_enabled;
42384272
dflash_capture->ubatch = &ubatch;
42394273
cparams.hidden_gpu_n_seqs = 0;
42404274
for (int s = 0; s < (int) LLAMA_DFLASH_MAX_SLOTS; ++s) {
@@ -5890,6 +5924,10 @@ void llama_set_dflash_capture(llama_context * ctx, const int32_t * layer_ids, in
58905924
ctx->set_dflash_capture(layer_ids, n_layers);
58915925
}
58925926

5927+
void llama_set_dflash_gpu_capture(llama_context * ctx, bool enabled) {
5928+
ctx->set_dflash_gpu_capture(enabled);
5929+
}
5930+
58935931
void llama_set_dflash_sample_temp(llama_context * ctx, float temp) {
58945932
ctx->set_dflash_sample_temp(temp);
58955933
}

src/llama-context.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ struct dflash_capture_data {
117117

118118
// tape recording (for DeltaNet state rollback)
119119
bool tape_enabled = false;
120+
bool gpu_capture_enabled = true;
120121
std::vector<int32_t> recurrent_layer_ids; // model layer indices that are DeltaNet
121122
std::unordered_map<std::string, std::pair<int, int>> tape_name_map; // name → (layer_idx, type)
122123
std::vector<dflash_tape_layer> tape_layers; // one per recurrent layer (CPU fallback)
@@ -474,6 +475,7 @@ struct llama_context {
474475

475476
// DFlash: configure hidden state capture layers
476477
void set_dflash_capture(const int32_t * layer_ids, int32_t n_layers);
478+
void set_dflash_gpu_capture(bool enabled);
477479
void set_dflash_sample_temp(float temp);
478480
void set_dflash_topk(int k);
479481
void set_dflash_verify_logits(bool enabled, int top_k);

0 commit comments

Comments
 (0)