Repository navigation
Misc. bug: CUDA ADD/GELU slower on B200 since PDL commit #30004
Description
Name and Version
ggml v0.25.3 (353b63b4) and llama.cpp master 7f2dd88 (2026-10-04). First bad: 7b28f950.
Operating systems
Linux
Which llama.cpp modules do you know to be affected?
libllama (core library)
Command line
Problem description & steps to reproduce
We upgraded ggml from v0.9.9 to v0.25.3 and found regression in the performance on CUDA ADD and GELU_ERF on a B200, which are 38–54% slower. GGML_CUDA_PDL=0 doesn't help, but the same commit built for 89-virtual is fast again.
The following probing experiments were done by an AI agent.
Standalone repro (public ggml API only), F32, median ms of 30 calls, 3 interleaved rounds, spread < 0.3%:
| op | 56b7420c (parent) | 7b28f950 | 7b28f950, GGML_CUDA_PDL=0 | 7b28f950, built for 89-virtual |
|---|---|---|---|---|
| ADD [2048,588,200] + [2048] | 0.633 | 0.977 | 0.975 | 0.650 |
| ADD [28,588,200] + [28,588,200] | 0.0146 | 0.0202 | 0.0205 | 0.0150 |
| GELU_ERF [2048,588,200] | 0.708 | 0.977 | 0.975 | 0.710 |
| FLASH_ATTN_EXT (D=64, 16 heads, 588 tokens, batch 200) | 7.87 | 7.87 | 7.87 | - |
test-backend-ops perf -o ADD -b CUDA0:
| case | 56b7420c | 7b28f950 |
|---|---|---|
| ADD(type=f32,ne=[4096,1,1,1],nr=[1,512,1,1]) | 6.14 us (3818 GB/s) | 9.41 us (2492 GB/s) |
Same numbers on v0.25.3 and llama.cpp master 7f2dd88: ADD 0.977, GELU_ERF 0.978 ms.
Also: FLASH_ATTN_EXT at the shape above is 6.84 ms on v0.9.9 and 7.90 ms on v0.25.3 / master. Unchanged across 7b28f950, not bisected.
Environment: NVIDIA B200 (sm_100), driver 610.43.02, CUDA 13.0, GCC 13.3.0. Build: -DCMAKE_BUILD_TYPE=Release -DGGML_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=100 (or 89-virtual for the last column).
Build and run, with a ggml checkout in ./ggml:
cmake -B build -DGGML_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=100
cmake --build build --target repro
build/repro all 30
CMakeLists.txt
cmake_minimum_required(VERSION 3.18)
project(ggml_repro CXX C)
set(CMAKE_CXX_STANDARD 17)
# ggml checked out at the commit under test, next to this file
add_subdirectory(ggml)
add_executable(repro repro.cpp)
target_link_libraries(repro PRIVATE ggml)repro.cpp
// Times single ggml ops on the CUDA backend at the shapes of a small
// vision-transformer world model (588 tokens, batch 200), through the public
// API only, so the same file builds against any ggml version.
//
// repro <op> [reps] op: add_bias | add_res | gelu_erf | concat | flash_attn | all
//
// Prints one JSON line per op: median and min milliseconds over reps, after
// warmup. Inputs are fixed pseudo-random values.
#include "ggml-alloc.h"
#include "ggml-backend.h"
#include "ggml-cuda.h"
#include "ggml.h"
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <functional>
#include <string>
#include <vector>
namespace {
constexpr int64_t kTokens = 196, kFrames = 3, kS = kTokens * kFrames, kB = 200;
constexpr int64_t kHeads = 16, kHeadDim = 64, kInner = kHeads * kHeadDim, kMlp = 2048, kDim = 28;
void fill(ggml_tensor* t) {
if (t->type == GGML_TYPE_F16) {
return; // set by the caller (the mask)
}
std::vector<float> v(ggml_nelements(t));
uint32_t s = 12345u;
for (auto& x : v) {
s = s * 1664525u + 1013904223u;
x = float(s >> 8) / float(1u << 24) - 0.5f;
}
ggml_backend_tensor_set(t, v.data(), 0, ggml_nbytes(t));
}
double run(ggml_backend_t backend, const char* name, int reps,
const std::function<ggml_tensor*(ggml_context*, std::vector<ggml_tensor*>&)>& build,
const std::function<void(ggml_tensor*)>& set = nullptr) {
ggml_init_params ip = {64 * ggml_tensor_overhead() + ggml_graph_overhead(), nullptr, true};
ggml_context* ctx = ggml_init(ip);
std::vector<ggml_tensor*> inputs;
ggml_tensor* out = build(ctx, inputs);
ggml_cgraph* gf = ggml_new_graph(ctx);
ggml_build_forward_expand(gf, out);
ggml_backend_buffer_t inbuf = ggml_backend_alloc_ctx_tensors(ctx, backend); // inputs (leafs)
ggml_gallocr_t ga = ggml_gallocr_new(ggml_backend_get_default_buffer_type(backend));
ggml_gallocr_alloc_graph(ga, gf);
for (ggml_tensor* t : inputs) {
if (set && t->type == GGML_TYPE_F16) {
set(t);
} else {
fill(t);
}
}
for (int i = 0; i < 3; ++i) {
ggml_backend_graph_compute(backend, gf);
}
ggml_backend_synchronize(backend);
std::vector<double> ms;
for (int i = 0; i < reps; ++i) {
auto t0 = std::chrono::steady_clock::now();
ggml_backend_graph_compute(backend, gf);
ggml_backend_synchronize(backend);
ms.push_back(std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - t0).count());
}
std::sort(ms.begin(), ms.end());
const double median = ms[ms.size() / 2];
std::printf("{\"op\":\"%s\",\"median_ms\":%.4f,\"min_ms\":%.4f,\"reps\":%d}\n", name, median, ms[0], reps);
std::fflush(stdout);
ggml_gallocr_free(ga);
ggml_backend_buffer_free(inbuf);
ggml_free(ctx);
return median;
}
ggml_tensor* input(ggml_context* ctx, std::vector<ggml_tensor*>& in, ggml_type type, int64_t a, int64_t b = 1,
int64_t c = 1, int64_t d = 1) {
ggml_tensor* t = ggml_new_tensor_4d(ctx, type, a, b, c, d);
in.push_back(t);
return t;
}
} // namespace
int main(int argc, char** argv) {
const std::string op = argc > 1 ? argv[1] : "all";
const int reps = argc > 2 ? std::atoi(argv[2]) : 50;
ggml_backend_t backend = ggml_backend_cuda_init(0);
if (backend == nullptr) {
std::fprintf(stderr, "no CUDA device\n");
return 2;
}
const bool all = op == "all";
double last = 0;
// the FFN bias add: [mlp, S, B] + [mlp]
if (all || op == "add_bias") {
last = run(backend, "add_bias", reps, [](ggml_context* c, std::vector<ggml_tensor*>& in) {
return ggml_add(c, input(c, in, GGML_TYPE_F32, kMlp, kS, kB), input(c, in, GGML_TYPE_F32, kMlp));
});
}
// the residual add: [dim, S, B] + [dim, S, B]
if (all || op == "add_res") {
last = run(backend, "add_res", reps, [](ggml_context* c, std::vector<ggml_tensor*>& in) {
return ggml_add(c, input(c, in, GGML_TYPE_F32, kDim, kS, kB), input(c, in, GGML_TYPE_F32, kDim, kS, kB));
});
}
// the FFN activation: GELU (erf) over [mlp, S, B]
if (all || op == "gelu_erf") {
last = run(backend, "gelu_erf", reps, [](ggml_context* c, std::vector<ggml_tensor*>& in) {
return ggml_gelu_erf(c, input(c, in, GGML_TYPE_F32, kMlp, kS, kB));
});
}
// the latent and action-token concat: [18, tokens, frames, B] ++ [10, tokens, frames, B] on dim 0
if (all || op == "concat") {
last = run(backend, "concat", reps, [](ggml_context* c, std::vector<ggml_tensor*>& in) {
return ggml_concat(c, input(c, in, GGML_TYPE_F32, 18, kTokens, kFrames, kB),
input(c, in, GGML_TYPE_F32, 10, kTokens, kFrames, kB), 0);
});
}
// flash attention over strided Q/K/V views of one fused [3*inner, S, B]
// projection, with a block-causal F16 mask repeated over the batch
if (all || op == "flash_attn") {
last = run(
backend, "flash_attn", reps,
[](ggml_context* c, std::vector<ggml_tensor*>& in) {
ggml_tensor* qkv = input(c, in, GGML_TYPE_F32, 3 * kInner, kS, kB);
auto split = [&](int64_t part) {
ggml_tensor* v = ggml_view_4d(c, qkv, kHeadDim, kHeads, kS, kB, kHeadDim * qkv->nb[0],
qkv->nb[1], qkv->nb[2], part * kInner * qkv->nb[0]);
return ggml_permute(c, v, 0, 2, 1, 3);
};
ggml_tensor* mask = input(c, in, GGML_TYPE_F16, kS, kS, 1, kB);
return ggml_flash_attn_ext(c, split(0), split(1), split(2), mask, 1.0f / std::sqrt(float(kHeadDim)),
0.0f, 0.0f);
},
[](ggml_tensor* mask) {
std::vector<ggml_fp16_t> m(ggml_nelements(mask));
for (int64_t b = 0; b < kB; ++b) {
for (int64_t q = 0; q < kS; ++q) {
for (int64_t k = 0; k < kS; ++k) {
const bool blocked = k / kTokens > q / kTokens;
m[(b * kS + q) * kS + k] = ggml_fp32_to_fp16(blocked ? -INFINITY : 0.0f);
}
}
}
ggml_backend_tensor_set(mask, m.data(), 0, ggml_nbytes(mask));
});
}
ggml_backend_free(backend);
return last > 0 ? 0 : 1;
}First Bad Commit
7b28f950 (Programmatic Dependent Launch (PDL) for more performance on newer NVIDIA GPUs (Hopper+), #22522)
Relevant log output
No response
Activity
am17an commentedon Oct 5, 2026
I'm not sure if any maintainer will have access to a B200 to check. But I will tag @aendk who added PDL in llama.cpp for support.
renmengye commentedon Oct 5, 2026
Thanks @am17an and @aendk
I ran a quick repro on an H200 and the regression is also observed.
NVIDIA H200 (sm_90), driver 610.43.02, CUDA 13.0, GCC 13.3.0. Same repro as above, F32, median ms of 30 calls, 3 interleaved rounds:
Build: -DCMAKE_BUILD_TYPE=Release -DGGML_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=90 (or 89-virtual for the last column).
aendk commentedon Oct 6, 2026
Will check this, in the coming days I can arrange to get access to H100/B200.
This is quite interesting and might be a bug. PDL kernels running in isolation should have identical performance to non-PDL kernels. PDL only unlocks a speed up by overlapping successive kernels, and this increases performance the more memory bandwidth you have - the H100/B200 setups should thus benefit the most (compared to consumer setups) when you run more than 1 kernel at a time.