feat(extraction): add CUDA language support (.cu/.cuh) (#387, #648) (#1172)

CUDA rides the C++ grammar via the Metal (#1121) dialect pattern:
blankCudaConstructs (offset-preserving) blanks execution-space specifiers
(__global__ family), __launch_bounds__(...), and <<<grid, block>>> launch
configs — which otherwise lex as shift operators and destroy the
host→kernel call edge entirely. Gated by .cu/.cuh extension OR by content
(looksLikeCudaSource), because much real CUDA lives in .h/.hpp headers:
cutlass launches most kernels from headers and flash-attention's launch
templates are .h. Safe by construction — no CUDA marker is valid C++
anywhere, and the launch blank is bounded + brace-balance-checked so a
stray <<< (committed merge-conflict markers) can never blank real code.

All real-world launch styles connect: plain, templated
(k<T, 256><<<...>>>), function-pointer (auto kernel = &fn<...>; with
branch reassignments each linked), dim3{...} brace-init configs, and
kernels defined through name-in-first-argument macros
(DEFINE_FLASH_FORWARD_KERNEL style — gtest TEST_F / PYBIND11_MODULE
shapes deliberately excluded by the two-lone-identifiers rule).

Two general C++ resolution wins the flow validation forced out:
- namespace blocks now prefix contained symbols' qualifiedNames
  (prefix-only — no namespace nodes, avoiding #1093-style crowd-out), so
  ns::fn(...) calls resolve; previously every namespace-qualified C++
  call was a permanently dead edge. cutlass: +30,864 edges (~10%), node
  count byte-identical.
- templated callees (fn<T, 256>(args)) strip template args at extraction
  (mirroring #1043 for base classes), so they match their definitions.

Validated on llm.c (165 host→kernel launch edges, was 0),
flash-attention (run_flash_fwd → flash_fwd_kernel → compute_attn traces
in one codegraph_explore call), and NVIDIA CUTLASS; fmt as the plain-C++
control (unchanged). A/B n=2/arm: Read/Grep displacement decisive on all
three repos (flash-attention Reads 29,13 → 5,2).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-07-03 17:41:45 -05:00
committed by GitHub
co-authored by Claude Fable 5
parent 1441933a26
commit e1a8d888e5
7 changed files with 662 additions and 8 deletions
+335 -1
View File
@@ -11,7 +11,7 @@ import * as os from 'os';
import { CodeGraph } from '../src';
import { extractFromSource, scanDirectory, buildDefaultIgnore, discoverEmbeddedRepoRoots, buildScopeIgnore } from '../src/extraction';
import { detectLanguage, isLanguageSupported, getSupportedLanguages, initGrammars, loadAllGrammars, isSourceFile } from '../src/extraction/grammars';
import { stripCppTemplateArgs, blankCppExportMacros, blankCppInlineMacros, blankMetalAttributes, recoverMangledCppName } from '../src/extraction/languages/c-cpp';
import { stripCppTemplateArgs, blankCppExportMacros, blankCppInlineMacros, blankMetalAttributes, blankCudaConstructs, recoverMangledCppName } from '../src/extraction/languages/c-cpp';
import { normalizePath } from '../src/utils';
beforeAll(async () => {
@@ -107,6 +107,13 @@ describe('Language Detection', () => {
expect(isSourceFile('Renderer/Shaders.metal')).toBe(true);
});
it('should detect CUDA files as C++ (#387)', () => {
expect(detectLanguage('kernels/scan.cu')).toBe('cpp');
expect(detectLanguage('include/reduce.cuh')).toBe('cpp');
expect(isSourceFile('csrc/flash_attn/softmax.cu')).toBe(true);
expect(isSourceFile('include/block_reduce.cuh')).toBe(true);
});
it('should detect Erlang files', () => {
expect(detectLanguage('src/my_server.erl')).toBe('erlang');
expect(detectLanguage('include/records.hrl')).toBe('erlang');
@@ -2946,6 +2953,333 @@ kernel void computeBlur(texture2d<float, access::read> inTexture [[texture(0)]],
});
});
describe('CUDA extraction (#387)', () => {
// CUDA parses with the C++ grammar. Three CUDA-only shapes misparse:
// execution-space specifiers (`__global__ void f(…)`) shunt the real return
// type into an ERROR node, `__shared__ float tile[256]` mangles the declared
// name to `float`, and — the critical one — `k<<<grid, block>>>(args)` lexes
// as shift operators around an empty-named template so NO call_expression
// (and therefore no host→kernel call edge) exists. blankCudaConstructs
// (preParse, `.cu`/`.cuh`-gated) blanks all three so extraction matches
// plain C++.
const CUDA = `#include <cuda_runtime.h>
#include "kernels.cuh"
__constant__ float d_scale[16];
__device__ __forceinline__ float warp_reduce_sum(float val) {
for (int offset = 16; offset > 0; offset /= 2) {
val += __shfl_down_sync(0xffffffff, val, offset);
}
return val;
}
__global__ void scale_kernel(float* out, const float* __restrict__ in, int n) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
__shared__ float tile[256];
if (i < n) {
tile[threadIdx.x] = in[i];
__syncthreads();
out[i] = warp_reduce_sum(tile[threadIdx.x]) * d_scale[0];
}
}
__global__ void __launch_bounds__(256, 4) bounded_kernel(float* data, int n) {
if (blockIdx.x * blockDim.x + threadIdx.x < n) data[0] *= 2.0f;
}
template <typename T, int BLOCK>
__global__ void templated_kernel(T* data, int n) {
if (blockIdx.x * BLOCK + threadIdx.x < n) data[0] += T(1);
}
class GpuBuffer {
public:
explicit GpuBuffer(size_t n) { cudaMalloc(&ptr_, n * sizeof(float)); }
~GpuBuffer() { cudaFree(ptr_); }
private:
float* ptr_ = nullptr;
};
void launch_scale(float* out, const float* in, int n, cudaStream_t stream) {
dim3 block(256);
dim3 grid((n + block.x - 1) / block.x);
scale_kernel<<<grid, block, 0, stream>>>(out, in, n);
bounded_kernel<<<grid,
block>>>(out, n);
templated_kernel<float, 256><<<grid, block>>>(out, n);
}
`;
it('extracts kernels, device functions, and host→kernel launch calls from a .cu file', () => {
const result = extractFromSource('kernels/scan.cu', CUDA);
expect(result.errors).toHaveLength(0);
const functions = result.nodes.filter((n) => n.kind === 'function').map((n) => n.name);
expect(functions).toEqual(
expect.arrayContaining([
'warp_reduce_sum',
'scale_kernel',
'bounded_kernel',
'templated_kernel',
'launch_scale',
])
);
expect(result.nodes.filter((n) => n.kind === 'class').map((n) => n.name)).toContain('GpuBuffer');
expect(result.nodes.find((n) => n.kind === 'import')?.name).toBe('cuda_runtime.h');
// No misparse artifacts: pre-blank, `__shared__ float tile[256]` parsed
// with `float` as the declared name. (Top-level C++ variables aren't
// extracted as nodes — matching plain-C++ behavior is the target.)
expect(result.nodes.map((n) => n.name)).not.toContain('float');
// Blanking is offset-preserving, so positions stay exact.
expect(result.nodes.find((n) => n.name === 'scale_kernel')!.startLine).toBe(13);
// THE point of CUDA support: every `<<<…>>>` launch form — plain,
// launch-bounds, multi-line config, and templated — emits a `calls`
// reference, so the host→kernel edge exists in the graph. Pre-blank,
// the chevrons lexed as shifts and none of these existed.
const calls = result.unresolvedReferences
.filter((r) => r.referenceKind === 'calls')
.map((r) => r.referenceName);
// The templated launch is normalized to the bare kernel name (template
// args stripped at extraction, like base-class extends refs — #1043), so
// it resolves to the kernel the template was defined as.
expect(calls).toEqual(
expect.arrayContaining([
'scale_kernel',
'bounded_kernel',
'templated_kernel',
'warp_reduce_sum',
])
);
});
it('blankCudaConstructs blanks every CUDA form, offset- and newline-preserving', () => {
const inp = [
'__global__ void __launch_bounds__(256, 4) step(float* p) {',
' __shared__ float tile[32];',
'}',
'__host__ __device__ int both() { return 0; }',
'void run(float* p, int n) {',
' step<<<grid,',
' block, 0, stream>>>(p);',
'}',
].join('\n');
const out = blankCudaConstructs(inp);
expect(out.length).toBe(inp.length); // every byte offset preserved
expect(out.split('\n').length).toBe(inp.split('\n').length); // newlines survive the multi-line launch config
expect(out).not.toMatch(/__global__|__launch_bounds__|__shared__|__host__|__device__|<<<|>>>/);
// Collapsing blank runs gives plain C++ back.
expect(out.split('\n').map((l) => l.replace(/ +/g, ' ').trimEnd())).toEqual([
' void step(float* p) {',
' float tile[32];',
'}',
' int both() { return 0; }',
'void run(float* p, int n) {',
' step',
' (p);',
'}',
]);
});
it('blankCudaConstructs never touches non-CUDA chevrons or identifiers', () => {
for (const c of [
'std::cout << "a" << b << c;', // shift chains — never three consecutive <
'auto x = f(a >> 3, b >> 3);', // right shifts
'std::vector<std::vector<std::vector<int>>> deep;', // template >>> closer with no <<< opener
'printf("<<<unterminated");', // <<< in a string with no >>> anywhere
'int __restrict__like = 1;', // dunder-ish identifier not in the specifier list
'int z = 1;', // nothing CUDA at all — early-return path
]) {
expect(blankCudaConstructs(c)).toBe(c);
}
// A stray `<<<` (committed merge-conflict marker) must not blank the code
// between markers. Two independent guards: statements between markers
// carry `;` (excluded from the span)…
const conflict = [
'<<<<<<< HEAD',
'int a = compute(1);',
'=======',
'int a = compute(2);',
'>>>>>>> feature-branch',
].join('\n');
expect(blankCudaConstructs(conflict)).toBe(conflict);
// …and a `;`-free region still fails the brace-balance check (the `{`s
// opened between the markers never close before the `>>>`).
const semicolonFree = [
'<<<<<<< HEAD',
'void foo() {',
'=======',
'void bar() {',
'>>>>>>> feature-branch',
].join('\n');
expect(blankCudaConstructs(semicolonFree)).toBe(semicolonFree);
});
it('blanks brace-initialized launch configs (`dim3{…}`), balanced-only', () => {
const inp = 'run_it<<<dim3{1, 2, 1}, dim3{256, 1, 1}, 0, stream>>>(data, n);';
const out = blankCudaConstructs(inp);
expect(out.length).toBe(inp.length);
expect(out.replace(/ +/g, ' ')).toBe('run_it (data, n);');
});
it('recovers the real kernel name from a macro-definition idiom, gtest/pybind untouched', () => {
const code = `#define DEFINE_MY_FWD_KERNEL(kernelName, ...) \\
template<typename Traits, __VA_ARGS__> \\
__global__ void kernelName(const Params params)
DEFINE_MY_FWD_KERNEL(fwd_kernel, bool Is_causal, int kBlockM) {
do_work(params);
}
TEST_F(MyFixture, HandlesEmptyInput) {
check(1);
}
`;
const result = extractFromSource('kernels/impl.cu', code);
const functions = result.nodes.filter((n) => n.kind === 'function').map((n) => n.name);
// The macro invocation's first argument is the defined name.
expect(functions).toContain('fwd_kernel');
expect(functions).not.toContain('DEFINE_MY_FWD_KERNEL');
// gtest's TEST_F(Fixture, Name) has TWO lone identifiers — ambiguous, so
// it keeps the macro name rather than guessing.
expect(functions).toContain('TEST_F');
expect(functions).not.toContain('MyFixture');
});
it('links launches through a local function pointer to the real kernel(s)', () => {
// The flash-attention launch-template shape end-to-end: a macro-defined
// kernel + `auto kernel = &fn<…>` + branch reassignment + launch through
// the local. The call refs must name the real kernels, not `kernel`.
const code = `template <typename T, bool Flag>
__global__ void fwd_kernel(T* data, int n) {
if (blockIdx.x * blockDim.x + threadIdx.x < n) data[0] += T(1);
}
template <typename T>
__global__ void fwd_splitkv_kernel(T* data, int n) {
if (blockIdx.x * blockDim.x + threadIdx.x < n) data[0] += T(2);
}
template <typename T>
void run_fwd(T* data, int n, cudaStream_t stream) {
auto kernel = &fwd_kernel<T, true>;
if (n % 2 == 0) {
kernel = &fwd_kernel<T, false>;
} else if (n % 3 == 0) {
kernel = &fwd_splitkv_kernel<T>;
}
kernel<<<(n + 255) / 256, 256, 0, stream>>>(data, n);
}
`;
const result = extractFromSource('kernels/launch.cu', code);
expect(result.errors).toHaveLength(0);
const calls = result.unresolvedReferences
.filter((r) => r.referenceKind === 'calls')
.map((r) => r.referenceName);
// Every DISTINCT branch target recorded once — the two fwd_kernel<…>
// instantiations strip to one target, the splitkv branch adds a second.
// The local's name never leaks as a callee.
expect(calls.filter((c) => c === 'fwd_kernel')).toHaveLength(1);
expect(calls.filter((c) => c === 'fwd_splitkv_kernel')).toHaveLength(1);
expect(calls).not.toContain('kernel');
});
it('CUDA blanking is gated by extension or content — plain C++ shift/template chevrons are untouched', () => {
const cpp = `#include <vector>
int shift_it(int a, int b) { return a << b << 1; }
std::vector<std::vector<std::vector<int>>> matrix() { return {}; }
`;
const result = extractFromSource('math.cpp', cpp);
expect(result.errors).toHaveLength(0);
const functions = result.nodes.filter((n) => n.kind === 'function').map((n) => n.name);
expect(functions).toEqual(expect.arrayContaining(['shift_it', 'matrix']));
});
it('CUDA in extension-less headers is caught by content: launch templates in .h connect host→kernel', () => {
// Much real CUDA lives in .h: cutlass launches most of its kernels from
// headers, flash-attention's launch templates are .h, llm.c keeps device
// helpers in C-detected .h. `looksLikeCudaSource` content-gates the same
// blank there — no CUDA marker is valid C++ anywhere, so this can't
// affect a genuinely-plain C++ header.
const header = `#pragma once
#include <cuda_runtime.h>
template <typename T>
__global__ void fill_kernel(T* out, T value, int n) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n) out[i] = value;
}
template <typename T>
void launch_fill(T* out, T value, int n, cudaStream_t stream) {
fill_kernel<T><<<(n + 255) / 256, 256, 0, stream>>>(out, value, n);
}
`;
const result = extractFromSource('include/fill_launch_template.h', header);
expect(result.errors).toHaveLength(0);
const functions = result.nodes.filter((n) => n.kind === 'function').map((n) => n.name);
expect(functions).toEqual(expect.arrayContaining(['fill_kernel', 'launch_fill']));
const calls = result.unresolvedReferences
.filter((r) => r.referenceKind === 'calls')
.map((r) => r.referenceName);
expect(calls).toContain('fill_kernel');
});
});
describe('C++ namespace qualifiedName prefixing', () => {
// C++ namespaces previously left no trace in qualifiedNames, so a
// namespace-qualified call (`flash::compute(...)`) could never match its
// definition — every `ns::fn()` call site was a permanently dead edge.
// The namespace name now prefixes contained symbols' qualifiedNames
// (prefix-only: no namespace node is minted — `namespace cutlass {` opens
// in thousands of files and a node per block would crowd search, #1093).
it('prefixes contained symbols and handles nesting; anonymous stays bare', () => {
const code = `namespace flash {
namespace detail {
void helper() {}
}
void compute_attn(int x) { detail::helper(); }
class Softmax {
public:
void rescale() {}
};
}
namespace {
void file_local() {}
}
void global_fn() { flash::compute_attn(1); }
`;
const result = extractFromSource('dispatch.cpp', code);
expect(result.errors).toHaveLength(0);
const byName = new Map(result.nodes.map((n) => [n.name, n]));
expect(byName.get('compute_attn')?.qualifiedName).toBe('flash::compute_attn');
expect(byName.get('helper')?.qualifiedName).toBe('flash::detail::helper');
expect(byName.get('Softmax')?.qualifiedName).toBe('flash::Softmax');
// Class scope still stacks under the namespace prefix.
expect(byName.get('rescale')?.qualifiedName).toBe('flash::Softmax::rescale');
// Anonymous namespace contents and true globals stay bare.
expect(byName.get('file_local')?.qualifiedName).toBe('file_local');
expect(byName.get('global_fn')?.qualifiedName).toBe('global_fn');
// The qualified call refs are emitted as spelled.
const calls = result.unresolvedReferences
.filter((r) => r.referenceKind === 'calls')
.map((r) => r.referenceName);
expect(calls).toEqual(expect.arrayContaining(['flash::compute_attn', 'detail::helper']));
});
it('C++17 nested namespace form prefixes as written', () => {
const code = `namespace a::b {
int f() { return 1; }
}
`;
const result = extractFromSource('nested.cpp', code);
expect(result.nodes.find((n) => n.name === 'f')?.qualifiedName).toBe('a::b::f');
});
});
describe('C++ forward declarations do not mint phantom class nodes (#1093)', () => {
// `class Foo;` parses as a bodiless class_specifier. Repeated across headers,
// each forward decl minted a phantom bodiless `class` node that crowded out —