Your first program#

Write it once. Run it on every GPU. Get the same bits.

At the end of this page you will have:

  • written a simulation in .vis;

  • run it on every GPU in the machine and confirmed that the result is bit-identical;

  • produced a receipt and a report, and checked the receipt without any GPU.

The outputs below were copied from a real run on a laptop with an RTX 4050, an Intel UHD and llvmpipe. On your machine the times and the device names change; the computed numbers do not.

1. Write it#

A metal plate with a heat source in the middle. Save it as plate.vis:

// A metal plate with a heat source in the middle: heat spreads to the neighbours.
param steps = 500
param alpha = 0.2              // share exchanged with each neighbour per step (stable below 0.25)

canvas plate[256, 256] edge clamp {
    temperature                // each channel is one number per pixel, starting at 0
    source
}

kernel heat on plate {
    if (x - 128)^2 + (y - 128)^2 < 100 {
        source = 1
    }
}

kernel diffuse on plate {
    let neighbours = temperature[x - 1, y] + temperature[x + 1, y]
                   + temperature[x, y - 1] + temperature[x, y + 1]
    temperature += alpha * (neighbours - 4 * temperature)
    if source > 0.5 {
        temperature = 100      // the source stays at 100 degrees
    }
}

measure average = mean(temperature) on plate
measure hot = count(temperature > 50) on plate

run {
    heat
    repeat steps { diffuse }
    average  hot
    print average
    print hot
    save plate.temperature "plate.png"
}

The four parts:

canvas

A grid of pixels (here 256 × 256) with channels, the numbers stored in each pixel. edge clamp says what is read outside the border: the nearest pixel. The other options are zero and wrap.

kernel

The code of one pixel, run on all of them at once on the GPU. x and y are its coordinates. It can read any neighbour (temperature[x - 1, y]) but only writes to its own pixel. Every pixel reads the state at the start of the step, so there are no data races.

measure

One number over the whole canvas (sum, mean, min, max, count), computed on the GPU in a fixed summation order.

run

The order of things.

2. Run it#

vision run plate.vis
device: NVIDIA GeForce RTX 4050 Laptop GPU — DiscreteGPU via Vulkan (driver: NVIDIA 595.99.02)
compiled for the device in 402 ms
average = 1.875969867
hot = 837
saved plate.png
501 kernel runs, 32.8 M pixels updated in 4.1 ms (7966 Mpx/s), 2 measurements

Pick another device with --device intel, --device cpu, --device numpy or the number shown by vision devices. Change a parameter without editing the file: --param steps=2000.

3. The same result on every device#

vision compare plate.vis
GPUs: time of the 2nd run (the 1st warms up the driver and is discarded)
device                                     time    Mpx/s   result
numpy (CPU, reference)                 332.2 ms       99   reference
NVIDIA GeForce RTX 4050 Laptop GPU       2.6 ms    12406   bit-identical
Intel(R) Graphics (RPL-P)               18.4 ms     1786   bit-identical
llvmpipe (LLVM 20.1.2, 256 bits)        40.7 ms      806   bit-identical

“Bit-identical” means that each of the 65 536 pixels and each measure has the same 32 bits on all four devices. In other GPU languages this does not happen, because every vendor rounds differently; see How determinism works.

4. Real data#

A small terrain as CSV, elevation.csv (semicolons with decimal commas also work):

12;8,5;3
15;9;30
canvas map[3, 2] { elevation  flooded }
kernel rain on map {
    if elevation < 10 {
        flooded = 1
    }
}
measure area = count(flooded > 0.5) on map
run {
    load map.elevation "elevation.csv"
    rain
    area
    print area
    print map.flooded
}
loaded map.elevation <- elevation.csv (CSV)
area = 3
map.flooded (3x2): min 0 | mean 0.5 | max 1

Vision also reads .npy, .asc (the Esri ASCII grid that GDAL exports from any GeoTIFF) and .png. To look at a file before writing the program:

vision info dem.asc
dem.asc: ASCII grid, cell 30, no data = -9999
resolution: 4 x 3 (width x height): declare canvas name[4, 3]
values: min 7 | mean 11.5 | max 16
2 cells without data: use load canvas.channel "dem.asc" nodata <number>
cell: 30 (unit of the file's coordinate reference system)
origin (corner of the lower-left cell): x = 345000.5, y = 7395000
Vision does not read the coordinate reference system; declare what the model uses, for example: param cell = 30.0

More in Real data.

5. Receipt, report and verification#

vision run plate.vis --receipt receipt.json --report report.html
vision verify receipt.json --device intel
vision verify receipt.json --device numpy
receipt: receipt.json (03568e29693c8bda…)
report: report.html

verifier: Intel(R) Graphics (RPL-P) | receipt made on: NVIDIA GeForce RTX 4050 Laptop GPU
MATCH: 4 outputs (maps and measures) bit-identical to the receipt

verifier: numpy (CPU, reference) | receipt made on: NVIDIA GeForce RTX 4050 Laptop GPU
MATCH: 4 outputs (maps and measures) bit-identical to the receipt
  • The receipt binds the program, the parameters, the data files, the Vision version and every output into one hash.

  • The verifier re-runs the whole simulation and compares bit by bit. With --device numpy it runs on Python and numpy only, without a GPU or a driver: whoever receives the receipt does not need to trust whoever made it.

  • A tampered receipt does not pass.

  • The report (--report) is a self-contained HTML page that opens offline and prints to PDF.

To prove who issued the receipt and when, see Receipts, signatures and reports.

Next steps#