Skip to content

Bias + GELU and neighbor aggregation: the mage-001 kernels, 1.4-1.7x behind Triton #59

Description

@superposition

Bias + GELU and neighbor aggregation: the mage-001 kernels, 1.4-1.7x behind Triton

These two operations still run the kernels written for the first comparison. Everything else has been revisited; these have not, and they are now the largest systematic deficit against a tuned baseline.

Numbers (kernel time, Nsight Systems, 100 iterations, same-session controls)

Operation cuda-oxide (mage-006) Triton PyTorch cuTile (mage-004)
Bias + GELU 4096x768 11.02 7.76 16.12 7.97
Neighbor aggregation 4096x64x65536 10.28 5.97 120.93 62.12

Event spans (mean of 300 samples, mage-006): GELU 13.96 against Triton's 28.35; neighbor 15.88 against Triton's 27.62. So the span column favours our kernels while the kernel column does not — the same pattern as LayerNorm before its rewrite, and the same caveat applies: the two columns come from different runs.

What both kernels do now

  • bias_gelu: one element per thread, thread::index_1d(), grid sized to the element count divided by 256. Each thread loads x[i] and bias[i % width], computes the tanh approximation, stores. All scalar, one pass, no reuse.
  • neighbor: one thread per output element with a serial loop over the row's edges, reading x, weights and the CSR arrays directly from global memory per element.

Neither has a tiled or vectorised path, and neither uses more than a thread's own registers.

What to try, in order

  1. Vectorised element access in both kernels. vector::as_vectors::<F32x4> gives 128-bit loads and stores; the GELU kernel is a pure elementwise map, so four elements per thread with one load and one store is the obvious shape. The bias index pattern (i % width) stays correct if the quad index is computed per lane group.
  2. Block per row for neighbor aggregation. The row's contribution is a reduction over rowptr-delimited edges; a block or warp per row with a shuffle reduction removes the per-element CSR reads that the current kernel repeats, and matches how PyTorch's index_add_ reference and the Triton kernel are shaped.
  3. Match the control's occupancy. The LayerNorm rewrite closed 18.64 -> 8.97 by copying Triton's shape (one block per row, 31 registers, one pass, minimal shared). Measure the register count and block shape of a candidate against Triton's, as that analysis did with ASTSource + cuobjdump on an offline compile.
  4. Only after those: cache hints on read-only loads, and a fused variant that writes in place.

Protocol

Same as the other gap issues: one measurement at a time on the shared GPU, a control kernel captured in the same session, kernel time and event span reported separately and named, and worse variants recorded here rather than dropped. See issue #53 for the shared-resource rules and docs/research/parallel-tracks-handoff.md for the current state of every track.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    gapA measured gap against a baseline, with evidence attachedworkstream:kernelsRust kernel optimisation track (main agent) — evidence in PRs, stage records in field notes

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions