Skip to content

Misc. bug: CUDA ADD/GELU slower on B200 since PDL commit #30004

Open

Description

@renmengye

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

  1. am17an commented on Oct 5, 2026

    @am17an
    Contributor

    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.

  2. renmengye commented on Oct 5, 2026

    @renmengye
    Author

    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:

    op 56b7420c (parent) 7b28f950 7b28f950, built for 89-virtual
    ADD [2048,588,200] + [2048] 0.714 1.141 0.714
    ADD [28,588,200] + [28,588,200] 0.0190 0.0254 0.0193
    GELU_ERF [2048,588,200] 0.766 1.142 0.766
    FLASH_ATTN_EXT (D=64, 16 heads, 588 tokens, batch 200) 9.59 9.59 9.59

    Build: -DCMAKE_BUILD_TYPE=Release -DGGML_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=90 (or 89-virtual for the last column).

  3. aendk commented on Oct 6, 2026

    @aendk
    Contributor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions