Why REX's GPU Benchmark Layer Must Not Become A Catch-All Test Suite

Posted on (Updated on )
REX needs real GPU benchmarks, but that does not mean the benchmark layer should become the place where every regression is first discovered. Parser drift, AST construction bugs, semantic-analysis mistakes, lowering invariant regressions, and CPU semantic drift belong in earlier, cheaper layers. The GPU benchmark layer works best when it stays narrow: it should ask whether the full offloading path still runs, whether current native LLVM and current REX still agree, whether the chosen timing source still measures the intended work, and whether a moved result came from the compiler, the runtime lifecycle, the measurement method, or the benchmark itself. When benchmarks are used as a catch-all oracle, the result is noise instead of clarity.

The previous post argued that real GPU benchmarks still matter in REX because they are the only place where the full offloading stack meets a real application.

That does not mean benchmarks should become the default place to detect every kind of bug.

This post is about that boundary.

The benchmark layer is indispensable, but only if it stays disciplined.

If it tries to answer every testing question at once, it becomes:

  • slow,
  • noisy,
  • hard to interpret,
  • and too expensive to use as a first line of defense.

So the question is not whether benchmarks matter.

The question is:

what is the benchmark layer actually for, and what should it refuse to become?

A diagram dividing earlier cheap test layers from the narrow benchmark layer, showing that cheaper layers catch local compiler issues and the benchmark layer catches full-application reality.

Figure 1. The benchmark layer is the top of the stack, not the whole stack. Its value comes from asking the questions that only a real application can answer.

Why A Catch-All Benchmark Layer Sounds Attractive

It is easy to see why teams are tempted to overuse benchmarks.

A real benchmark run feels authoritative.

It is the full application. It uses the real inputs. It exercises the real GPU path. It produces numbers and outputs that look concrete.

That creates a seductive idea:

1
if benchmarks are the final truth, maybe they should be the main truth too

In practice, that is usually the wrong trade.

A benchmark can absolutely tell you that something is wrong.

It is often a terrible place to learn what kind of thing is wrong for the first time.

If the benchmark layer becomes the primary detector for parser drift, AST mistakes, clause-normalization issues, host-code structural regressions, timing mistakes, runtime-lifecycle bugs, and application-level output drift all at once, then a failed benchmark row stops being informative.

It just means:

1
something somewhere in the whole system changed

That is not a good testing strategy. That is a very expensive debugging starting point.

The Earlier Layers Exist To Remove Cheap Ambiguity

REX already has a layered testing strategy precisely to prevent that problem.

Parser-level tests should catch:

  • directive spelling and grammar drift,
  • callback-sequence breakage,
  • and OpenMPIR construction mistakes.

Frontend AST-construction tests should catch:

  • incorrect SgOmp* nodes,
  • lost clauses,
  • bad combined-construct handling,
  • and frontend crashes on realistic directive mixtures.

Semantic-analysis checks should catch:

  • data-sharing misunderstandings,
  • clause interpretation problems,
  • and analysis normalization issues.

Lowering invariant tests should catch:

  • missing helper calls,
  • wrong offload-entry counts,
  • broken launch-block structure,
  • misplaced rex_offload_init(),
  • bad map-array shapes,
  • and similar artifact-level regressions.

CPU equivalence tests should catch:

  • semantic drift in lowered host code,
  • especially for constructs where the GPU is not even the first question yet.

Those layers are cheap compared with a full benchmark run.

More importantly, they are cleaner.

Each one narrows the blame surface before the benchmark layer ever runs.

That is not bureaucracy. It is what makes the benchmark layer interpretable later.

What The Benchmark Layer Is Bad At Catching First

A good rule of thumb is:

if the issue can be stated as a local compiler artifact question, a benchmark is usually the wrong first detector.

That includes things like:

  • parser drift,
  • clause spelling mistakes,
  • analysis normalization bugs,
  • or a simple missing lowering artifact.

A benchmark may fail because of those bugs.

But if the first signal comes from a benchmark, the signal is worse than it should have been.

Instead of seeing:

1
missing offload entry in generated host file

you see:

1
benchmark row no longer runs

Instead of seeing:

1
rex_offload_init moved after the timer declaration

you see:

1
one application became inexplicably slower

That is exactly the kind of ambiguity the earlier layers are meant to eliminate.

The benchmark layer should not be proud of catching those things first. That usually means the cheaper layers were asked too little.

A routing diagram showing local compiler questions on the left and real application questions on the right, with a route box connecting them, illustrating that the right question should go to the right layer.

Figure 2. The right question should go to the right layer. Benchmarks should receive the questions that require a real application, not every question the compiler can possibly fail.

What The Benchmark Layer Is Actually For

The benchmark layer earns its cost by asking the questions that the earlier layers cannot answer well.

In practice, those questions are narrow and concrete.

1. Does the full offloading path still run?

This is the irreducible top-layer question.

It includes:

  • generated host launch code,
  • generated device code,
  • helper files,
  • linked runtime libraries,
  • registered device images,
  • launch packets,
  • application-owned control flow,
  • and the benchmark’s actual inputs.

No earlier layer composes all of that at once.

2. Do current native LLVM and current REX still agree?

This is the core correctness question at the benchmark layer.

It is not just “did the program run?”

It is:

  • after using the right output mode,
  • after applying narrow output normalization,
  • does current REX still match current native LLVM on a real application?

That question belongs at the top because only real applications expose the mix of output conventions, floating-point behavior, and runtime structure that made this distinction necessary.

3. Does the timing source still mean what the benchmark row claims?

The benchmark layer also owns measurement interpretation.

It has to ask:

  • is this line really a comparable runtime?
  • is this wall-clock proxy dominated by non-GPU work?
  • does the benchmark already expose a better kernel-specific timing line?
  • should this row really be judged with profiler-derived GPU totals instead?

That is why the benchmark layer caught timing-source mistakes for pathfinder and b+tree. That was not a parser issue, a lowering issue, or even strictly a runtime issue. It was a top-layer measurement issue.

4. If results moved, what category of cause is it?

The most valuable thing a good benchmark layer can do is help classify change.

When a row moves, the benchmark layer should help answer whether the move came from:

  • the compiler,
  • the runtime lifecycle,
  • the measurement method,
  • or the benchmark’s own output or timing conventions.

That is a very different purpose from being the first place every bug is discovered.

The benchmark layer is most useful when it explains believable movement, not when it merely screams that some movement happened.

What Happens When Benchmarks Become A Catch-All Oracle

When benchmarks are asked to do everything, several bad things happen at once.

First, failure triage gets slower.

A benchmark failure usually has a huge blame surface:

  • parser,
  • AST,
  • semantic analysis,
  • lowering,
  • helper files,
  • runtime integration,
  • measurement methodology,
  • or the application itself.

If earlier layers did not already narrow those possibilities, the benchmark result becomes an expensive ambiguity machine.

Second, signal quality drops.

A benchmark row can be affected by:

  • input-sensitive runtime behavior,
  • noisy timing,
  • non-semantic output formatting,
  • baseline drift,
  • or cold-start effects.

That is acceptable when the benchmark layer is only asked the questions that require all of that context.

It is a bad trade when the same row is also expected to be the first detector for basic compiler-local regressions.

Third, developer behavior gets worse.

If the first meaningful signal only arrives at the benchmark layer, engineers are pushed toward the wrong habits:

  • debugging from giant end-to-end artifacts,
  • chasing symptoms before structural causes,
  • or “fixing the benchmark” instead of fixing the narrower regression that should have been caught earlier.

That is how benchmark suites turn into folklore instead of engineering tools.

The Healthy Shape Of The Top Layer

A healthy benchmark layer is not broad. It is selective.

It should assume that:

  • the parser already works,
  • the frontend already builds the right OpenMP AST,
  • semantic normalization has already been checked,
  • lowering invariants already cover structural regressions,
  • CPU equivalence already protects meaning on the host side.

With those assumptions in place, the benchmark layer can stay focused on the questions that remain genuinely unresolved:

  • does the full GPU offloading path still survive a real application?
  • do current native LLVM and current REX still agree under that real application?
  • do the performance conclusions still hold under a fair metric?
  • and if not, what kind of top-layer issue is this?

That shape is narrow enough to stay interpretable and strong enough to stay necessary.

A before-and-after diagram showing a noisy catch-all benchmark layer on one side and a disciplined high-value benchmark layer on the other.

Figure 3. Benchmark value comes from selectivity. When the layer asks only real-application questions, its failures become expensive but meaningful instead of broad and noisy.

Why This Discipline Makes The Benchmark Layer More Powerful, Not Less

It may sound as if narrowing the benchmark layer would make it less important.

It does the opposite.

A benchmark row becomes more credible when readers know that cheaper layers have already removed simpler explanations.

If a benchmark still fails after parser tests, AST checks, semantic checks, lowering invariants, and CPU equivalence all passed, then the remaining explanations are already more interesting:

  • full runtime lifecycle placement,
  • measurement-source quality,
  • output-mode visibility,
  • baseline drift,
  • or real end-to-end integration.

That makes the benchmark layer a better final reality check, because its failures now point to genuinely top-layer issues instead of every possible compiler issue at once.

This is also why the benchmark layer should be described as an investigation layer rather than a scoreboard.

Its best output is not merely:

1
2
3
4
REX faster
LLVM faster
pass
fail

Its best output is:

1
this row moved, and here is the class of reason that explains the movement

That is much more valuable.

The Design Rule In One Sentence

REX’s GPU benchmark layer should be the final place where real applications validate the whole assembled offloading path, not the first place where ordinary compiler-local regressions are discovered.

That is why the layer must stay narrow:

  • cheaper layers should catch cheap ambiguities,
  • and the benchmark layer should spend its cost only on full-application questions.

That discipline is what keeps the top layer from becoming a noisy catch-all test suite and preserves it as the one layer that still tells the truth when everything else already looked fine.