Skip to content

[Bug] Fix bench_kineto returning zero on the first call in a process - #426

Open
DiegoCao wants to merge 1 commit into
deepseek-ai:mainfrom
DiegoCao:fix/bench-kineto-first-call-empty-table
Open

DiegoCao wants to merge 1 commit into
deepseek-ai:mainfrom
DiegoCao:fix/bench-kineto-first-call-empty-table

Conversation

@DiegoCao

@DiegoCao DiegoCao commented Sep 2, 2026

Copy link
Copy Markdown

Summary

bench_kineto returns 0 for every kernel on the first call in a process. The first torch.profiler.profile(activities=[CUDA]) session records no device activity at all — key_averages() comes back empty, nothing matches any kernel name, and the parser falls through to its else 0 branch.

Since the first bench_kineto call in each test file feeds a division, six of our own test files abort four frames away from the cause:

Testing GEMM:
Traceback (most recent call last):
  File "tests/test_bf16.py", line 245, in <module>
    test_gemm()
  File "tests/test_bf16.py", line 49, in test_gemm
    f'{2 * m * n * k / t / 1e12:4.0f} TFLOPS | '
       ~~~~~~~~~~~~~~^~~
ZeroDivisionError: division by zero

test_bf16, test_fp8_fp4, test_einsum, test_attention, test_hyperconnection and test_legacy all fail this way. test_layout survives only because it happens to guard its division — it silently reports its first shape as 0 us | 0 GB/s instead.

Root cause

This is a property of the profiler, not of DeepGEMM. It reproduces with no DeepGEMM code at all, and only the first session in the process is affected:

import torch
P = torch.profiler.ProfilerActivity
a = torch.randn(4096, 4096, device='cuda', dtype=torch.bfloat16)
b = torch.randn(4096, 4096, device='cuda', dtype=torch.bfloat16)

def run(i):
    sch = torch.profiler.schedule(wait=0, warmup=1, active=1, repeat=1)
    p = torch.profiler.profile(activities=[P.CUDA], schedule=sch, acc_events=True)
    with p:
        for _ in range(2):
            for _ in range(30): a @ b
            torch.cuda.synchronize(); p.step()
    print(f'call {i}: table lines =', len(p.key_averages().table().split('\n')))

for i in range(4): run(i)
call 0: table lines = 1     <-- empty
call 1: table lines = 10
call 2: table lines = 10
call 3: table lines = 10

I tried each candidate fix in a fresh process:

variant result
baseline (wait=0, warmup=1, active=1) empty
more warmup (wait=1, warmup=2, active=1, 4 iters) empty
no schedule at all empty
add ProfilerActivity.CPU works
one throwaway profiler session first works

More warmup steps do not help, so this is CUPTI lazy initialization at session level rather than a schedule problem. Adding ProfilerActivity.CPU also works, but it puts CPU op rows in the table where they can match kernel-name substrings and trip the existing uniqueness assertion — so this PR uses the throwaway session. Its with body is empty, so it runs no GPU work and costs nothing.

Changes

  1. _warmup_kineto() — open and close one empty profiler session, once per process, before the measured session.
  2. Assert the profiling table is non-empty. An absent kernel name legitimately means "this kernel did not run" (e.g. the reduce name in test_bf16 when there is no split-K), so that case must keep returning 0; but a table with no device activity at all is always a profiling failure and should say so rather than report every kernel as infinitely fast. test_layout already wraps its call in try/except AssertionError, so it is unaffected.

Verification

Measured end-to-end on GB200 / sm_100a: capability (10, 0), get_arch_major() == 10, cubins built for sm_100 (cuobjdump --list-elf -> kernel.sm_100.cubin), aarch64, CUDA 13.1, PyTorch 2.9.0+cu130, third-party/cutlass at the pinned f3fde58.

Determinism. Unpatched tests/test_bf16.py fails 3/3 independent runs with the identical traceback. Patched, it passes.

Recovered data point. tests/test_layout.py, first row:

- > Perf (num_groups= 1, mn= 4096, k=  128, transpose=1, use_ue8m0=0, gran_k= 32):    0 us |    0 GB/s
+ > Perf (num_groups= 1, mn= 4096, k=  128, transpose=1, use_ue8m0=0, gran_k= 32):    3 us |   49 GB/s

3 us | 49 GB/s is what the same shape measures later in the same run, and every subsequent row is unchanged.

Paired first-call check against a real sm100 kernel, at test_bf16's actual first shape and at a large square:

pre-fix  m=1 n=2112 k=7168  call 0: no device activity recorded
pre-fix  m=1 n=2112 k=7168  call 1: t =  20.30 us
pre-fix  m=1 n=2112 k=7168  call 2: t =  20.27 us
fixed    m=1 n=2112 k=7168  call 0: t =  20.10 us
fixed    m=1 n=2112 k=7168  call 1: t =  20.40 us
fixed    m=1 n=2112 k=7168  call 2: t =  20.42 us

Post-fix call 0 agrees with calls 1 and 2 to within run-to-run noise: the warmup recovers the measurement without shifting it.

Rows produced. Perf rows emitted, and how many of them read 0.0 us:

test file before after zero-time rows after
test_bf16 0 (aborted) 272 0
test_fp8_fp4 0 (aborted) 384 0
test_einsum 0 (aborted) 50 0

test_bf16 also now reports Average speedup over cuBLASLt: 0.894x, an aggregate that previously could not be computed at all. Coverage spans the sm100 GEMM family: sm100_bf16_gemm, sm100_bf16_k_grouped_gemm, sm100_bf16_m_grouped_gemm_contiguous, sm100_bf16_m_grouped_gemm_masked, sm100_fp8_fp4_gemm_1d1d, sm100_tf32_hc_prenorm_gemm, sm100_bmn_bnk_mn_gemm.

Out of scope

Two test files fail on this machine both before and after, for reasons unrelated to this bug:

  • test_mega_moe needs at least 6 GPUs (ValueError: device_id cuda:5 is out of range); this box has 4.
  • test_sanitizer hits a JIT assertion under compute-sanitizer's build flags: not std::regex_search(output, std::regex(R"(Local memory used)")) while compiling fp8_gemm_nt_skip_head_mid, i.e. that kernel spills local memory. Reported separately if useful.

🤖 Generated with Claude Code

The first `torch.profiler.profile(activities=[CUDA])` session in a process
records no device activity: `key_averages()` comes back empty, no line matches
any kernel name, and `bench_kineto` silently returns 0.

Because the first `bench_kineto` call in each test file feeds a division, six
of our own test files abort on a `ZeroDivisionError` four frames away from the
cause:

    File "tests/test_bf16.py", line 49, in test_gemm
      f'{2 * m * n * k / t / 1e12:4.0f} TFLOPS | '
         ~~~~~~~~~~~~~~^~~
    ZeroDivisionError: division by zero

test_bf16, test_fp8_fp4, test_einsum, test_attention, test_hyperconnection and
test_legacy all fail this way. test_layout survives only because it happens to
guard its division, and instead silently reports its first shape as
`0 us | 0 GB/s`.

This is a property of the profiler, not of DeepGEMM. It reproduces with no
DeepGEMM code at all, and only the first session in the process is affected:

    def run(i):
        sch = torch.profiler.schedule(wait=0, warmup=1, active=1, repeat=1)
        p = torch.profiler.profile(
            activities=[torch.profiler.ProfilerActivity.CUDA],
            schedule=sch, acc_events=True)
        with p:
            for _ in range(2):
                for _ in range(30): a @ b
                torch.cuda.synchronize(); p.step()
        print(i, len(p.key_averages().table().split('\n')))

    for i in range(4): run(i)   # -> 0 1  /  1 10  /  2 10  /  3 10

Adding profiler warmup steps does not help, and neither does dropping the
schedule; opening and closing one throwaway (empty) session does. So this is
CUPTI lazy initialization rather than a schedule problem. Adding
`ProfilerActivity.CPU` also fixes it, but that puts CPU op rows in the table
where they can match kernel-name substrings and trip the existing uniqueness
assertion, so this uses the throwaway session instead. It runs no GPU work at
all and leaves every measured number unchanged.

Also assert that the table is non-empty. An absent kernel name legitimately
means "this kernel did not run" (e.g. the `reduce` name in test_bf16 when there
is no split-K), so that case must keep returning 0; but a table with no device
activity at all is always a profiling failure and should say so rather than
report every kernel as infinitely fast. test_layout already wraps its call in
`try/except AssertionError`, so it is unaffected.

Measured on GB200 (sm_100a, aarch64, cubins built for sm_100), CUDA 13.1,
PyTorch 2.9.0+cu130, third-party/cutlass at the pinned f3fde58.

  - unpatched tests/test_bf16.py fails 3/3 runs with the identical traceback;
    patched, it passes and reports 272 perf rows, none of them 0.0 us, and an
    aggregate "Average speedup over cuBLASLt: 0.894x" that previously could not
    be computed at all because the run aborted on the first shape.
  - tests/test_layout.py first row: `0 us | 0 GB/s` -> `3 us | 49 GB/s`, which
    is what the same shape measures later in the same run; every later row is
    unchanged.
  - paired first-call check on a real sm100 kernel, both at m=1,n=2112,k=7168
    and m=n=k=4096: pre-fix call 0 records nothing while calls 1 and 2 are
    fine; post-fix call 0 agrees with calls 1 and 2 to within run-to-run noise.
@DiegoCao DiegoCao changed the title Fix bench_kineto returning zero on the first call in a process [Bug] Fix bench_kineto returning zero on the first call in a process Sep 2, 2026
@DiegoCao

DiegoCao commented Sep 2, 2026

Copy link
Copy Markdown
Author

@LyricZhao @soundOfDestiny Could you help review? Thanks.



_is_kineto_warmed_up = False

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 suggestion: _is_kineto_warmed_up is a module-level flag with a check-then-set pattern; it is not thread-safe. Fine for the current single-threaded test usage, but if bench_kineto is ever called from multiple threads, two profiler sessions could be opened concurrently (torch.profiler does not support nested/concurrent sessions). Consider a threading.Lock or a once-style guard if that ever becomes a concern.

🤖 v5

# NOTES: an empty table means no device activity was recorded at all; without this
# check every kernel below would silently parse as a zero time
assert any(line.strip() for line in prof_lines), 'The profiler recorded no device activity'
kernel_names = (kernel_names, ) if isinstance(kernel_names, str) else kernel_names

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 suggestion: The assertion any(line.strip() for line in prof_lines) catches the fully-empty-table case, but it would also be satisfied by a table containing only header rows with no kernel rows (if a future torch version emits headers for empty results). A slightly stronger check (e.g. that at least one line contains a time unit such as 'us' or 'ms') would be more robust, though the current check is adequate for the observed failure mode.

🤖 v5

@ds-review-bot

Copy link
Copy Markdown
Collaborator

🤖 ds-review-bot Code Review

v6

变更以一次性空 profiler 会话初始化 CUPTI,并在设备活动完全缺失时明确失败;未发现会破坏现有行为的缺陷。

v5

This MR fixes a real, well-diagnosed bug: the first torch.profiler CUDA-only session in a process records no device activity (CUPTI lazy initialization), so bench_kineto's parser fell through to its else 0 branch and six test files aborted with ZeroDivisionError. The fix adds a once-per-process _warmup_kineto() that opens/closes an empty profiler session before the measured one, plus an assertion that the parsed table is non-empty so a profiling failure raises instead of silently reporting every kernel as 0. The change is minimal (23 added lines in deep_gemm/testing/bench.py), well-commented, keeps the legitimate 'kernel name absent -> 0' behavior, and is backed by thorough verification on GB200 (deterministic repro, first-call measurements now matching later calls, test_layout's first row recovered from 0 us | 0 GB/s to real numbers). Alternative fixes (more warmup steps, adding CPU activity) were evaluated and correctly rejected. Also verified as a nice detail: _warmup_kineto() is called inside the suppress() context, so any throwaway-session noise is suppressed along with the real session. Approve; two non-blocking suggestions below.

v4p

该 MR 修复 bench_kineto 在同一进程中首次调用时,因 CUPTI 惰性初始化导致首个 CUDA profiler 会话不记录任何设备活动、从而所有 kernel 时间被解析为 0 的问题:通过进程级一次性空 profiler 预热,并在解析前增加表非空断言。整体实现简洁,符合既有代码模式,未发现明确的功能或正确性问题。

Files reviewed: 1
Issues found: 🔵 2 suggestion
Inline comments posted: 2

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants