Method tests are small fixture-based checks attached to one @GPU method with @GPUTest.
They are meant to answer a practical question: "Can this generated GPU method accept these inputs and produce this expected output?" They also provide optional evidence for future backend/device selection.
Method tests are not a replacement for JUnit. Think of them as portable GPU-kernel fixtures that JavaToGpu can inspect, materialize, compare against a CPU reference, and optionally run through a backend.
| Goal | Use |
|---|---|
| Check fixture files and parameter names | Metadata, fixture readiness, and value binding |
| Compare expected output without a GPU | Invocation materialization + CPU/reference comparison |
| Test a real backend/device explicitly | Optional GPU probe after a runtime scope is installed |
| Reuse probe evidence for placement experiments | Persistent probe cache and cache-only ranking |
The default workflow is manual today: examples and tests call the probe APIs explicitly. Runtime auto-gating from @GPUTest evidence is future/opt-in work, not a hidden first-launch behavior.
Run the numeric example:
.\gradlew.bat :examples-app:runMethodTestProbeExample --console=plainRun the struct example:
.\gradlew.bat :examples-app:runMethodTestStructProbeExample --console=plainRun the cache-only backend/device ranking walkthrough:
.\gradlew.bat :examples-app:runMethodTestProbeEvidenceRankingExample --console=plainThe first two examples do not require a real GPU. They inspect generated metadata, load JSON fixtures, create Java invocation arguments, and run a CPU-reference comparison. The ranking example also uses a synthetic backend so it can show persistent probe evidence without depending on local OpenCL hardware.
A method test has four normal stages:
- Metadata - load
@GPUTestentries from the generatedIrGpuartifact. - Fixture readiness - find JSON fixture resources on the classpath and verify the top-level JSON shape.
- Value binding - match fixture fields to kernel parameter names.
- Reference comparison - materialize Java arguments and compare output values with a CPU/reference callback.
There is also an optional fifth stage:
- GPU probe - run the materialized invocation through the currently installed runtime backend and compare the real GPU output.
The common safe path is stages 1-4. It is useful in tests and examples because it does not allocate GPU buffers, compile OpenCL, or require hardware.
@GPUTest stores references and metadata. It does not store Java objects directly.
@GPUTest(
id = "scale-smoke",
inputs = {"fixtures/scale-smoke.inputs.json"},
expectedOutputs = {"fixtures/scale-smoke.outputs.json"},
tolerance = "abs=1e-5",
tags = {"selection", "smoke"},
selectionProbe = true
)| Field | What it means |
|---|---|
id |
Stable name for this case. Use short names such as scale-smoke or vec2-scale-smoke. |
inputs |
Classpath JSON resources containing input values. |
expectedOutputs |
Classpath JSON resources containing expected read-write output values. |
tolerance |
Optional numeric comparison tolerance, for example abs=1e-5. |
tags |
Free-form labels for grouping, reporting, or selection-probe intent. |
selectionProbe |
Whether this fixture may be used as backend/device placement evidence. Use false for cases that are too slow or too large. |
You can add more than one @GPUTest to the same method. Keep the first fixtures tiny and boring; bigger edge-case fixtures are better once the basic path is green.
Attach @GPUTest to a normal @GPU method:
import net.sixik.ga_utils.javatogpu.api.GPU;
import net.sixik.ga_utils.javatogpu.api.annotations.GPUGlobal;
import net.sixik.ga_utils.javatogpu.api.annotations.GPUTest;
public final class ScaleKernel {
@net.sixik.ga_utils.javatogpu.api.annotations.GPU
@GPUTest(
id = "scale-smoke",
inputs = {"fixtures/method-test-probe/scale-smoke.inputs.json"},
expectedOutputs = {"fixtures/method-test-probe/scale-smoke.outputs.json"},
tolerance = "abs=1e-5",
tags = {"selection", "smoke"}
)
public static void scaleKernel(
@GPUGlobal float[] input,
@GPUGlobal float[] output
) {
int id = GPU.get_global_id(0);
output[id] = input[id] * 2.0f;
}
}The input fixture is a JSON object. Field names must match method parameter names:
{
"input": [1.0, 2.0, 3.0, 4.0]
}The expected-output fixture uses the read-write output parameter name:
{
"output": [2.0, 4.0, 6.0, 8.0]
}Then inspect and compare the fixture at runtime:
GpuKernelDescriptor descriptor = GpuGeneratedLauncherInvoker.descriptor(ScaleKernel.class, "scaleKernel");
ClassLoader classLoader = ScaleKernel.class.getClassLoader();
GpuRuntimeMethodTestProbePlan plan = GpuRuntimeMethodTestProbes.plan(descriptor, classLoader);
GpuRuntimeMethodTestFixtureReadiness readiness = GpuRuntimeMethodTestProbes.fixtureReadiness(plan, classLoader);
GpuRuntimeMethodTestFixtureValueBindingPlan bindings = GpuRuntimeMethodTestProbes.fixtureValueBindings(
descriptor,
plan,
classLoader
);
GpuRuntimeMethodTestInvocationMaterializationPlan materialization =
GpuRuntimeMethodTestProbes.fixtureInvocationMaterialization(descriptor, bindings);
GpuRuntimeMethodTestReferenceComparisonPlan referenceComparison = GpuRuntimeMethodTestProbes.compareWithReference(
materialization,
plan,
invocationArguments -> {
float[] input = (float[]) invocationArguments[0];
float[] output = (float[]) invocationArguments[1];
for (int index = 0; index < input.length; index++) {
output[index] = input[index] * 2.0f;
}
}
);Each plan has toMarkdown() and artifactFields(...) methods. Use toMarkdown() for humans and artifactFields(...) for reports, logs, or CI artifacts.
Struct method tests use ordinary JSON objects. The object field names must match the Java @GPUStruct fields.
Define a struct type with a no-arg constructor and writable fields:
import net.sixik.ga_utils.javatogpu.api.annotations.GPUStruct;
@GPUStruct
public final class Vec2 {
public double x;
public double y;
public Vec2() {
}
}Attach @GPUTest to a kernel that uses Vec2[]:
@net.sixik.ga_utils.javatogpu.api.annotations.GPU
@GPUTest(
id = "vec2-scale-smoke",
inputs = {"fixtures/method-test-struct-probe/vec2-scale-smoke.inputs.json"},
expectedOutputs = {"fixtures/method-test-struct-probe/vec2-scale-smoke.outputs.json"},
tolerance = "abs=1e-9",
tags = {"selection", "struct", "smoke"}
)
public static void scaleStructKernel(
@GPUGlobal Vec2[] input,
float scale,
@GPUGlobal Vec2[] output
) {
int id = GPU.get_global_id(0);
output[id].x = input[id].x * scale;
output[id].y = input[id].y * scale;
}The input fixture can contain both the struct array and scalar parameters:
{
"input": [
{ "x": 1.0, "y": 2.0 },
{ "x": 3.0, "y": 4.0 }
],
"scale": 2.0
}The expected-output fixture contains the read-write struct output:
{
"output": [
{ "x": 2.0, "y": 4.0 },
{ "x": 6.0, "y": 8.0 }
]
}Reference comparison flattens struct fields into deterministic paths such as [0].x, [0].y, [1].x, and [1].y. This makes mismatch diagnostics readable instead of reporting an opaque binary struct blob.
Use these rules first. They keep fixtures easy to debug and easy to cache.
- The root JSON value must be an object.
- Top-level keys should match method parameter names.
- Input fixtures can include read-only, read-write, and scalar value parameters.
- Expected-output fixtures should include read-write output parameters.
- Numeric fixture values may be scalars or arrays:
1.0,[1.0, 2.0],4,[1, 2, 3]. - Struct fixture values may be objects or arrays of objects.
- Struct fields may be primitive numeric values or nested
@GPUStructobjects. - Arrays inside
@GPUStructfields are not supported in the current OpenCL ABI. - Strings, booleans, nulls, mixed arrays, and object shapes that do not match the Java parameter type are rejected.
Supported primitive fixture types today are:
float, double, int, long, short, byte
float[], double[], int[], long[], short[], byte[]
@GPUStruct
@GPUStruct[]
For integer Java types, fixture numbers must be integral. For example, 3 is valid for int, while 3.5 is not.
Use tolerance when floating-point output may differ slightly between CPU reference and GPU/runtime output.
Common forms:
@GPUTest(tolerance = "abs=1e-5")
@GPUTest(tolerance = "rel=1e-4")
@GPUTest(tolerance = "abs=1e-5,rel=1e-4")abs is an absolute tolerance. rel is relative to the expected value. If no tolerance is provided, comparison is exact.
The CPU-reference preflight does not run the GPU. To run the same materialized fixtures through the currently installed runtime backend, call executeGpuProbe(...):
GpuRuntimeMethodTestGpuProbePlan gpuProbe = GpuRuntimeMethodTestProbes.executeGpuProbe(
descriptor,
materialization,
plan,
GpuRuntimeMethodTestGpuProbeOptions.cached()
);Only use this after installing a backend, for example through an OpenCL runtime scope. Keep probe fixtures small because the GPU probe is intended for smoke validation and placement evidence, not full benchmarking.
try (GpuScope ignored = JavaToGpu.useOpenClSharedCache()) {
GpuRuntimeMethodTestGpuProbePlan gpuProbe = GpuRuntimeMethodTestProbes.executeGpuProbe(
descriptor,
materialization,
plan,
GpuRuntimeMethodTestGpuProbeOptions.cached()
);
}For backend/device ranking, warm evidence explicitly and then let selection consume the cache:
GpuRuntimeMethodTestProbeEvidenceWarmup.warmSelectionProbeEvidence(
descriptor,
classLoader,
candidates,
GpuRuntimeMethodTestGpuProbeOptions.persistentCached(cacheDirectory)
);
GpuRuntimeCompileOptions rankingOptions =
GpuRuntimeCompileOptions.defaults(GpuBackendTarget.OPENCL)
.withPersistentMethodTestProbeEvidenceRanking(cacheDirectory, Duration.ofDays(7));The ranking policy is cache-only. It does not secretly run probes during device selection. maxEntryAge has two effects:
expired entries become cache misses, and old-but-still-valid entries are down-weighted in backend/device score
diagnostics before they expire.
For application code on the built-in OpenCL runtime, the shortest high-level helper is warmAndSelectOpenCl(...). It
keeps the same boundary, but returns one auditable report containing discovery, explicit warm-up, and follow-up
cache-only device selection:
GpuRuntimeMethodTestProbeEvidenceSelectionPlan selection =
GpuRuntimeMethodTestProbeEvidenceSelection.warmAndSelectOpenCl(
descriptor,
MyKernel.class.getClassLoader(),
GpuRuntimeMethodTestGpuProbeOptions
.persistentCached(Path.of(".javatogpu/method-test-probes"))
.withCompileOptions(baseOptions),
baseOptions,
2
);
System.out.println(selection.toMarkdown());warmAndSelectOpenCl(...) discovers OpenCL devices, creates warm-up candidates with
GpuRuntimeMethodTestProbeEvidenceWarmupCandidates.openClGpuDevices(...), runs only caller-approved warm-up probes, and
then performs cache-only device selection. Devices that were discovered but not warmed still appear as missing and
neutral, instead of pretending they were tested.
During that one-call flow the runtime emits lifecycle events for the selection boundary, OpenCL discovery, chosen
warm-up candidates, and final cache-only selection result. This is meant for optional journal/trace services: implement
GpuRuntimeLifecycleService, register it through META-INF/services, and the application can observe what happened
without switching to manual listener registration or letting a listener mutate placement.
GpuRuntimeMethodTestProbeEvidenceWarmupCandidates.openClGpuDevices(...) is the convenience path for the built-in
OpenCL runtime: it orders discovered devices by the policy ranking when available, skips CPU devices, caps the candidate
count, and creates owned OpenCL backends lazily for the explicit warm-up phase.
If you already have profiles from another discovery layer, use the lower-level warmAndSelect(...) overload that accepts
either a GpuRuntimeDeviceDiscoveryResult plus explicit warm-up candidates or a plain List<GpuRuntimeDeviceProfile>.
Warm-up candidates are pinned to their deviceProfile.deviceId while the probe runs. That means a real OpenCL warm-up
candidate records evidence for the same device that later appears in cache-only ranking, instead of accidentally using
whatever device the native runtime would have selected by default.
For a real OpenCL walkthrough, run:
.\gradlew.bat :examples-app:runOpenClMethodTestProbeEvidenceSelectionExample --console=plainThe example discovers OpenCL devices, creates owned OpenCL backend candidates for discovered GPU devices, warms only
those explicit candidates, and then prints the cache-only selection report. Use
-Pjavatogpu.methodTestProbeOpenClEvidenceCacheDir=... for a persistent cache path and
-Pjavatogpu.methodTestProbeOpenClWarmupLimit=1 when you want to warm only the first eligible GPU.
It also enables service-based lifecycle output by default: runtime-lifecycle.jsonl contains the full runtime event
journal, while opencl-evidence-selection.trace is a short human-readable trace. The console output filters that trace
to the warmAndSelectOpenCl(...) boundary so you can see discovery, candidate selection, and final placement without
reading the whole JSONL file. Override paths with -Pjavatogpu.lifecycleJournalFile=... and
-Pjavatogpu.exampleLifecycleTraceFile=....
For the curated OpenCL release walkthrough, run:
.\gradlew.bat :examples-app:runOpenClPracticalReleaseExample --console=plainThat walkthrough stitches together backend/device explanation, portable @GPUTest CPU-reference preflight, real OpenCL
probe evidence warm-up, cache-only placement, launch-shape guidance, vector/struct/packed-root-blob/image workload
smoke, image-helper guidance, optimizer artifact review guidance, and lifecycle trace output. Use
-Pjavatogpu.practicalOpenClEvidenceCacheDir=... to choose where the evidence cache and journals are written.
It also points to OpenClImageWorkflow.rgbaIntToFloat2D(...) as the short host-side path for the common 2D RGBA image
case.
The optimizer review section points to the separate runOptimizationJournalExample command and explains which
original.backend.opencl-c, optimized.backend.opencl-c, selected backend.opencl-c, and
runtime-ir-optimizer-evidence.properties files to inspect.
Runtime method-test integration is intentionally off by default. This keeps ordinary application startup predictable:
@GPUTest metadata can exist in generated artifacts without changing backend or device selection.
The first runtime integration mode is CACHE_ONLY:
GpuRuntimeCompileOptions options = GpuRuntimeCompileOptions
.defaults(GpuBackendTarget.OPENCL)
.withMethodTestProbeMode(GpuRuntimeMethodTestProbeMode.CACHE_ONLY);CACHE_ONLY means: use already-recorded selection-probe evidence if it is present, but never compile or execute a
probe during device selection. In most apps you will use the convenience method instead, because it also points the
runtime at the persistent cache directory:
GpuRuntimeCompileOptions options = GpuRuntimeCompileOptions
.defaults(GpuBackendTarget.OPENCL)
.withPersistentMethodTestProbeEvidenceRanking(Path.of(".javatogpu/method-test-probes"));This is enough for runtime selection to consume a warmed cache. Legacy withMethodTestProbeEvidenceRankingCached() and
runtime.methodTestProbeEvidenceRanking=cached still map to the same CACHE_ONLY behavior. Unknown mode values fail
closed instead of being ignored, so a future typo such as run-before-first-invoke will reject compile options with a
clear diagnostic rather than unexpectedly running or trusting probes.
Pass withPersistentMethodTestProbeEvidenceRanking(path, maxEntryAge) when stale evidence should lose score weight or
expire. Candidate diagnostics include freshness fields such as freshnessPermille, ageLimited, oldestAgeMillis, and
maxAgeMillis.
Automatic modes such as "run probe before first invoke" are deliberately future work. They need stronger guardrails for latency, cache invalidation, backend ownership, and user consent.
Most failures are intentionally plain strings so they can be logged and stored in CI artifacts.
| Blocker | Meaning | Fix |
|---|---|---|
fixture-resource-not-found |
A fixture path was not found on the classpath. | Put the JSON under resources and check the path in @GPUTest. |
fixture-payload-json-invalid |
The file is not valid JSON. | Validate the JSON and keep the root as an object. |
fixture-value-parameter-not-found |
No fixture field matched a method parameter. | Rename the JSON key or method parameter. |
fixture-value-shape-mismatch |
JSON shape does not match Java type. | Use scalar for scalar, array for array, object for struct, array of objects for struct array. |
fixture-value-java-type-unsupported |
The parameter type cannot be materialized by fixtures. | Use supported numeric types or @GPUStruct / @GPUStruct[]. |
fixture-value-integral-number-required |
A Java integer type received a fractional JSON number. | Use 3 instead of 3.5. |
fixture-value-struct-field-missing |
A required struct field is absent in JSON. | Add the missing field with the same name as the Java field. |
fixture-value-struct-field-type-unsupported |
The struct contains a field fixtures cannot materialize. | Use primitive numeric fields or nested @GPUStruct; move arrays out to kernel parameters. |
fixture-reference-output-mismatch |
CPU/reference output differs from expected fixture output. | Check the expected JSON, reference callback, and tolerance. |
- Numeric example:
examples-app/src/main/java/net/sixik/ga_utils/examples/MethodTestProbeExample.java - Numeric fixtures:
examples-app/src/main/resources/fixtures/method-test-probe/ - Struct example:
examples-app/src/main/java/net/sixik/ga_utils/examples/MethodTestStructProbeExample.java - Struct fixtures:
examples-app/src/main/resources/fixtures/method-test-struct-probe/ - Ranking example:
examples-app/src/main/java/net/sixik/ga_utils/examples/MethodTestProbeEvidenceRankingExample.java - Real OpenCL ranking example:
examples-app/src/main/java/net/sixik/ga_utils/examples/OpenClMethodTestProbeEvidenceSelectionExample.java - Lower-level runtime details: Runtime Guide
- Start with a tiny fixture: two to four elements.
- Run
fixtureReadiness(...)and fix classpath/resource problems first. - Run
fixtureValueBindings(...)and fix field names/types next. - Run
fixtureInvocationMaterialization(...)and inspect argument kinds. - Add
compareWithReference(...)and tune tolerance only when needed. - Add
executeGpuProbe(...)only after the CPU-reference preflight is green. - Use persistent probe evidence only for explicit warm-up or CI/device-selection flows.
This keeps problems small and makes each failure point easy to understand.