Docs/Compute

One language. Several physical shapes.

Begin with a borrowed Flow. Keep the same graph and move into Program, resident execution, Pipeline, or Batch only when the workload exposes the corresponding repeated cost.

<rund/compute.hpp>runD::sdkBounded DSL

Start with Flow

on(target, input) borrows an lvalue contiguous range. Construction copies no payload. Intermediate operations remain lazy, and collect() is the complete terminal that opens the selected backend, compiles, transfers, executes, and reads back.

Flow chain · contextual fragment
auto result = rund::compute::on(target, input)
    .map("adjust", [](auto value) { return value + 1; })
    .scan(rund::compute::Scan::InclusiveSum)
    .collect();

The scalar language is bounded

The lambda constructs a canonical expression circuit from fixed-width integer and Fixed values. Bounded selection, append, work queues, indexed reads and writes, grouping, collectives, and explicit unrolling compose a finite graph before execution.

  • Every input type and count is exact.
  • Every capacity and iteration ceiling is known before compile.
  • Overflow and unsupported expression shapes are typed admission failures.
  • No backend callback, shader handle, or hidden CPU implementation enters the public Flow.

The primary chain includes map, filter, scan, reduce, sort, argsort, compact, histogram, window, gather, scatter, partition, segmented collectives, bounded expand/join, group-by, records, multiple outputs, and explicit unrolling. Add <rund/compute/math.hpp> for composite expressions, matrices, transforms, factorization, solve, and spectrum stages.

Choose by repeated cost

SurfaceUse it whenIdentity that stays fixed
collect()You need one convenient host result.Flow graph, law, target, result.
ProgramThe graph repeats with changing input.Compiled canonical Program.
JobYou need explicit bindings and execution lifecycle.Program plus bounded input shape.
resident()State should remain on the selected device.Program and explicit Buffer state.
PipelineDependent Programs share resident intermediates.Declaration order, hazards, commit.
BatchIndependent jobs should share one submission boundary.Job order and per-job result identity.

Compile once when the graph repeats

A Program keeps the canonical graph and compiled backend artifact while inputs change. It executes again on every call; this is reuse, not result memoization.

program.cpp · checked installed example
#include <rund/compute.hpp>

#include <algorithm>
#include <array>
#include <cstdint>
#include <span>
#include <utility>

int main() {
  constexpr std::size_t Count = 4u;
  auto device = rund::compute::open(rund::compute::Target::cpu(1u));
  if (!device) {
    return device.exit_code();
  }

  auto flow = rund::compute::on(*device).map<std::int32_t>(
      "affine", Count, [](auto value) { return value * 2 + 5; });
  auto program = std::move(flow).compile();
  if (!program) {
    return program.exit_code();
  }

  constexpr std::array<std::int32_t, Count> Initial{1, 2, 3, 4};
  constexpr std::array<std::int32_t, Count> Changed{10, 20, 30, 40};
  auto first = program->run(std::span<const std::int32_t>{Initial});
  auto second = program->run(std::span<const std::int32_t>{Changed});
  if (!first) {
    return first.exit_code();
  }
  if (!second) {
    return second.exit_code();
  }

  constexpr std::array<std::int32_t, Count> First{7, 9, 11, 13};
  constexpr std::array<std::int32_t, Count> Second{25, 45, 65, 85};
  return std::ranges::equal(*first, First) &&
                 std::ranges::equal(*second, Second)
             ? 0
             : 2;
}

Overlap compilation only when you have other work

Add <rund/compute/async.hpp> and open the Device with an explicit Compile worker and queue budget. compile_async() consumes the Flow and returns immediately after admission; the future yields the same Program result as synchronous compile().

async.cpp · checked installed example
#include <rund/compute.hpp>
#include <rund/compute/async.hpp>

#include <array>
#include <cstdint>
#include <span>
#include <vector>

int main() {
  constexpr std::size_t Count = 4u;
  auto device = rund::compute::open(
      rund::compute::Target::cpu(1u),
      rund::compute::Compile{.workers = 1u, .capacity = 2u});
  if (!device) {
    return device.exit_code();
  }

  auto pending =
      rund::compute::on(*device)
          .map<std::int32_t>("async-adjust", Count,
                             [](auto value) { return value * 2 + 5; })
          .compile_async();
  if (!pending) {
    return pending.exit_code();
  }

  auto program = pending->get();
  if (!program) {
    return program.exit_code();
  }

  constexpr std::array<std::int32_t, Count> input{1, 2, 3, 4};
  auto output = program->run(std::span<const std::int32_t>{input});
  if (!output) {
    return output.exit_code();
  }
  return *output == std::vector<std::int32_t>{7, 9, 11, 13} ? 0 : 2;
}

Check both boundaries. A failed outer result means the recipe was not admitted; inspect reason() for unavailable service or full capacity and retry only after fixing that resource condition. A failed result from future::get() is the actual graph compilation failure; fix the reported graph or target condition. Neither path switches target.

Keep data resident only when reuse pays

Resident Job

resident loop · contextual fragment
auto job = program->resident(input);
if (!job) return job.exit_code();

for (std::size_t i = 0; i < iterations; ++i) {
  auto completed = job->run();
  if (!completed) return completed.exit_code();
}
auto output = job->read();

Resident creation prepares bounded storage and uploads the initial input. Warm run() changes execution state without an implicit readback; read() is the explicit host boundary.

Batch independent GPU Jobs

Batch accepts at most 64 resident Jobs from the same opened Device and executes them behind one native submission boundary. It does not fuse their graphs, change order, or support CPU Jobs. Empty, duplicate, cross-Device, busy, and over-capacity admission fail before any Job executes.

Batch submission · contextual fragment
rund::compute::Batch batch;
if (auto added = batch.add(*first); !added) return added.exit_code();
if (auto added = batch.add(*second); !added) return added.exit_code();
if (auto completed = batch.run(); !completed) return completed.exit_code();

auto first_output = first->read();
auto second_output = second->read();

Pipeline dependent Programs

Include <rund/compute/pipeline.hpp> when compiled Programs share resident buffers. Declare stages in order, bind exact reads and writes, call prepare() once, reuse run(), then publish state only through explicit commit/readback. Hazards, claims, poison, and failure remain tied to declaration order.

two-stage Pipeline · contextual fragment
auto prepared =
    rund::compute::pipeline(device)
        .then(*produce, rund::compute::read(*input),
              rund::compute::write(*middle))
        .then(*consume, rund::compute::read(*middle),
              rund::compute::write(*output))
        .prepare();
if (!prepared) return prepared.exit_code();

rund::compute::Pipeline pipeline = std::move(prepared).value();
if (auto ran = pipeline.run(); !ran) return ran.exit_code();
if (auto read = pipeline.read(*output, observed); !read) {
  return read.exit_code();
}

Publish tile output without making it state

Use the two-Program tile_repeat<0>(seed, fold) form when a window needs no inner Action. A Fold may return recurrent final values followed by tile-sized append-only values. Bind the first group with write_final(...) and the second with write_window(...). runD keeps only a private tile bank and publishes the exact active slice after that Fold succeeds; a partial tail never exposes padding or turns the appended values into next-window state. A later step in the same Pipeline can read that target directly after the nested group is sealed. The planner inserts the exact device write-to-read boundary without a host round trip, second Pipeline, or full-size shadow copy; later writes to the owned target remain rejected.

action-free tile output · contextual fragment
auto body = rund::compute::tile_repeat<0u>(*seed, *fold);
auto prepared =
    rund::compute::pipeline(device)
        .windows<Maximum, Tile>(
            body, rund::compute::window(*active_count),
            rund::compute::read(*outer_state, *source, *lanes),
            rund::compute::write_final(*final_state),
            rund::compute::write_window(*per_item_output))
        .then(*consume,
              rund::compute::read(*per_item_output),
              rund::compute::write(*consumed_output))
        .prepare();

Choose a checkpoint at the ownership boundary

LatestDeviceState is the copyable live hand-off for the same exact Device. It follows successful generations without copying or hashing payload bytes. For recovery on a different Device or backend, allocate one move-only SnapshotStorage, reuse snapshot_into(storage) at deliberate export points, and restore from its portable host payload. Neither path runs implicitly during run(), so code that requests no host feedback or checkpoint keeps the resident warm path.

checkpoint boundary · contextual fragment
rund::compute::LatestDeviceState latest =
    pipeline.latest_device_state().value();
if (auto restored = peer.restore(latest); !restored) {
  return restored.exit_code();
}

rund::compute::SnapshotStorage storage =
    pipeline.snapshot_storage().value();
if (auto saved = pipeline.snapshot_into(storage); !saved) {
  return saved.exit_code();
}

The names above stand for already compiled Programs, resident Buffers, and caller-owned output storage. For a larger profile-and-memory example, inspect the optional checked Pipeline consumer source ↗.

Submit a prepared Job through an active Session

Add <rund/compute/session.hpp> when the application already owns a runtime Session. Prepare the resident Job first, then submit that same Job; Session schedules it but does not recompile, change target, or read the result back.

compute-session.cpp · checked installed example
#include <rund/compute.hpp>
#include <rund/compute/session.hpp>
#include <rund/session.hpp>

#include <array>
#include <cstdint>
#include <vector>

int main() {
  constexpr std::array<std::int32_t, 4u> input{1, 2, 3, 4};
  auto program =
      rund::compute::on(rund::compute::Target::cpu(2u))
          .map<std::int32_t>("session-adjust", input.size(),
                             [](auto value) { return value * 2 + 5; })
          .compile();
  if (!program) {
    return program.exit_code();
  }

  auto job = program->resident(input);
  if (!job) {
    return job.exit_code();
  }

  int operation = 0;
  const rund::Session::Result hosted =
      rund::run(rund::SessionConfig{.workers = 2u},
                [&](rund::Session &session) {
                  const rund::compute::Completion completed =
                      session.compute(*job).submit().wait();
                  operation = completed.exit_code();
                });
  if (!hosted) {
    return hosted.exit_code();
  }
  if (operation != 0) {
    return operation;
  }

  auto output = job->read();
  if (!output) {
    return output.exit_code();
  }
  return *output == std::vector<std::int32_t>{7, 9, 11, 13} ? 0 : 2;
}

Keep the two outcomes separate: Completion owns submission and backend execution failure, while Session::Result owns lifecycle and drain failure. Read the Job only after both succeed. A busy Job must finish before resubmission; a stopped Session requires a new active lifecycle, not a standalone or CPU fallback.

Explicit target selection

The current Alpha exposes CPU, Metal, and Vulkan target constructors. These are current lowering implementations, not the identity of the Flow language. An additional backend must implement the same graph, numeric, ordering, failure, and evidence contracts before it can join.

target constructors · contextual fragment
Target::cpu()
Target::metal()
Target::vulkan()
No fallback Target selection is a product decision. An unavailable target fails with machine-readable state instead of running the computation somewhere else.

Use the focused public surface

Use focused <rund/*.hpp> entries and link only runD::sdk. Kernel scheduling, accelerator adapters, native device values, and Node runtime integration remain implementation layers behind the installed contract.

Choose by measured cost, or open API & Errors for focused extensions, result ownership, and failure actions.

Next decisionMeasure the physical boundary you selected