Control asynchronous execution
An ops:: call dispatches work and returns; the kernels run behind it. Where the work lands follows one law: a stream you pass is used as given; an operation placed on a bare Device resolves to the ambient placement scope's stream for that device when one is set, otherwise to the calling thread's asynchronous stream; and an operation with no placement of its own follows its inputs. Most programs never notice any of this, because reads wait for the result. This guide is for when you need control anyway: measuring where time goes, reacting the moment a result is ready, stepping op by op while debugging, or building a whole graph before running any of it.
Everything below runs on CPU streams, so it behaves the same on any machine; the same rules apply to CUDA, Vulkan and Metal streams. The timing numbers are from one real run and vary with the machine; the ordering they show does not. The C samples abbreviate the api-table bootstrap that tutorial part 1 shows in full.
Dispatch is not execution
An ops:: call returns as soon as the work is queued, and the result knows where it queued: Tensor::stream() names the stream the operation rode (under the asynchronous default, stream().is_default() is false), which is the stream to query and synchronize. Tensor::status() names where a result is in that lifecycle, Stream::query_idle() asks a stream without blocking, and Stream::synchronize() blocks until everything queued has run. Before dispatching anything, StreamOrDevice(device).resolve() reads where a bare-Device placement would land. Host reads (to_string, item, item_as_vec) wait on the producing work themselves, so a read is always safe; what you never observe is unfinished bytes.
- C++
- C
- Python
- Kotlin
- Go
- Rust
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <ClikaRT/clika_rt.h>
using ClikaRT::DataType;
using ClikaRT::Device;
using ClikaRT::Stream;
using ClikaRT::Tensor;
namespace ops = ClikaRT::ops;
namespace {
double ms_since(std::chrono::steady_clock::time_point t0) {
return std::chrono::duration<double, std::milli>(
std::chrono::steady_clock::now() - t0).count();
}
} // namespace
int main() {
// The inputs settle on a stream of their own. The chain below carries no
// placement, so it rides the calling thread's asynchronous lane, and its
// result names that stream: x.stream() is what to query and synchronize.
Stream worker = Stream::create(Device::cpu());
const Tensor a = ops::full({1024, 1024}, 0.001, DataType::Float32, worker);
const Tensor w = ops::full({1024, 1024}, 0.001, DataType::Float32, worker);
worker.synchronize(); // settle the inputs so only the chain is measured
const auto t0 = std::chrono::steady_clock::now();
Tensor x = a;
for (int i = 0; i < 24; ++i) x = ops::matmul(x, w);
const double dispatch_ms = ms_since(t0);
Stream lane = x.stream(); // the stream the chain actually ran on
std::printf("dispatch returned after %.1f ms, stream idle: %s\n",
dispatch_ms, lane.query_idle() ? "yes" : "no");
lane.synchronize();
std::printf("ready after %.1f ms, stream idle: %s\n",
ms_since(t0), lane.query_idle() ? "yes" : "no");
// A host read needs none of the above; it waits on its producer alone.
// (item<T>() reads a single-element tensor, so reduce first.)
std::printf("max(x) = %g\n", ops::amax(x).item<float>());
return 0;
}
#include <stdio.h>
#include <time.h>
#include "clika_rt/clika_rt_core.h"
/* api, check(): the bootstrap from tutorial part 1 (dlopen + clika_rt_get_api). */
extern const clika_rt_api* api;
extern void check(clika_rt_error* e, const char* where);
static double ms_since(struct timespec t0) {
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
return (now.tv_sec - t0.tv_sec) * 1e3 + (now.tv_nsec - t0.tv_nsec) / 1e6;
}
/* full({1024, 1024}, value) placed on a stream: shape entries and the fill
* value travel as scalar_or_tensor slots at this surface. */
static clika_rt_tensor* full_on(clika_rt_stream* s, double value) {
const clika_rt_scalar_or_tensor shape[2] = {
{.kind = CLIKA_RT_SCALAR_OR_TENSOR_INT, .int_value = 1024},
{.kind = CLIKA_RT_SCALAR_OR_TENSOR_INT, .int_value = 1024},
};
const clika_rt_scalar_or_tensor fill = {.kind = CLIKA_RT_SCALAR_OR_TENSOR_DOUBLE,
.double_value = value};
clika_rt_stream_or_device on = {.kind = CLIKA_RT_STREAM_OR_DEVICE_STREAM, .stream = s};
const clika_rt_stream_or_device none = {CLIKA_RT_STREAM_OR_DEVICE_DEFAULT};
clika_rt_tensor* t = NULL;
check(api->op_full(shape, 2, fill, CLIKA_RT_DATA_TYPE_FLOAT32, on, none, &t), "full");
return t;
}
int main(void) {
/* The inputs settle on a stream of their own. The chain below carries no
* placement, so it rides the calling thread's asynchronous lane; the result
* names that stream (tensor_stream hands out an owned reference), and that
* is the stream to query and synchronize. */
clika_rt_stream* worker = NULL;
check(api->stream_create(api->device_cpu(0), &worker), "stream_create");
clika_rt_tensor* a = full_on(worker, 0.001);
clika_rt_tensor* w = full_on(worker, 0.001);
check(api->stream_synchronize(worker), "sync"); /* settle the inputs */
struct timespec t0;
clock_gettime(CLOCK_MONOTONIC, &t0);
clika_rt_tensor* x = a;
for (int i = 0; i < 24; ++i) {
clika_rt_tensor* next = NULL;
api->tensor_retain(w); /* matmul consumes both operands */
check(api->op_matmul(x, w, NULL, 0, CLIKA_RT_ACTIVATION_IDENTITY,
0, 0, 0.0, 0.0, &next), "matmul");
x = next;
}
const double dispatch_ms = ms_since(t0);
clika_rt_stream* lane = api->tensor_stream(x); /* the stream the chain ran on */
printf("dispatch returned after %.1f ms, stream idle: %s\n",
dispatch_ms, api->stream_query_idle(lane) ? "yes" : "no");
check(api->stream_synchronize(lane), "sync");
printf("ready after %.1f ms, stream idle: %s\n",
ms_since(t0), api->stream_query_idle(lane) ? "yes" : "no");
api->stream_release(lane);
/* A host read needs none of the above; it waits on its producer alone. */
clika_rt_tensor* m = NULL;
check(api->op_amax(x, NULL, 0, 0, &m), "amax");
const void* p = NULL;
check(api->tensor_const_data_ptr(m, &p), "data_ptr");
printf("max(x) = %g\n", *(const float*)p);
api->tensor_release(m);
api->tensor_release(w);
api->stream_release(worker);
return 0;
}
Placing work on explicit worker streams is part of the C++ API today; the C++ tab shows the pipelined-stream pattern. Python calls are asynchronous by construction: an operation dispatches and returns, and a host read waits for the value (the devices-and-async chapter of the getting-started guide walks it).
import io.clika.runtime.ClikaRtGen
// Streams ride the generated handle tier; a tensor placed on one does too.
fun fullOn(stream: Long, value: Double): Long =
ClikaRtGen.tensorFull(longArrayOf(1024, 1024), value,
ClikaRtGen.DATA_TYPE_FLOAT32,
ClikaRtGen.STREAM_OR_DEVICE_STREAM, 0, 0, stream,
ClikaRtGen.STREAM_OR_DEVICE_DEFAULT, 0, 0, 0L)
fun main() {
ClikaRtGen.load()
// Ops on a NON-default stream run on a worker, not inline on this thread.
val cpu = ClikaRtGen.deviceCpu(0)
val worker = ClikaRtGen.streamCreate(
ClikaRtGen.deviceApiOf(cpu), ClikaRtGen.deviceIndexOf(cpu))
val a = fullOn(worker, 0.001)
val w = fullOn(worker, 0.001)
ClikaRtGen.streamSynchronize(worker) // settle the inputs
val t0 = System.nanoTime()
var x = a
repeat(24) {
ClikaRtGen.tensorRetain(w) // matmul consumes both operands
x = ClikaRtGen.opMatmul(x, w, 0L, false,
ClikaRtGen.ACTIVATION_IDENTITY, false, false, 0.0, 0.0)
}
val dispatchMs = (System.nanoTime() - t0) / 1e6
// The chain carried no placement, so it rode the calling thread's
// asynchronous lane; the result names that stream (an owned handle).
val lane = ClikaRtGen.tensorStream(x)
println("dispatch returned after %.1f ms, stream idle: %s"
.format(dispatchMs, if (ClikaRtGen.streamQueryIdle(lane)) "yes" else "no"))
ClikaRtGen.streamSynchronize(lane)
println("ready after %.1f ms, stream idle: %s"
.format((System.nanoTime() - t0) / 1e6,
if (ClikaRtGen.streamQueryIdle(lane)) "yes" else "no"))
ClikaRtGen.streamRelease(lane)
// A host read needs none of the above; it waits on its producer alone.
val m = ClikaRtGen.opAmax(x, longArrayOf(), false)
println("max(x) = ${ClikaRtGen.tensorToString(m)}")
ClikaRtGen.tensorRelease(m)
ClikaRtGen.tensorRelease(w)
ClikaRtGen.streamRelease(worker)
}
package main
import (
"fmt"
"log"
"time"
"github.com/Clika/clika_runtime/bindings/go/clikart"
)
func must[T any](v T, err error) T {
if err != nil {
log.Fatal(err)
}
return v
}
func check(err error) {
if err != nil {
log.Fatal(err)
}
}
// full({1024, 1024}, value) placed on a stream: the shape entries and the
// fill value travel as scalar-or-tensor slots at this surface.
func fullOn(api *clikart.Api, s *clikart.Stream, value float64) *clikart.Tensor {
shape := []clikart.ScalarOrTensor{
{Kind: clikart.ScalarOrTensorInt, IntValue: 1024},
{Kind: clikart.ScalarOrTensorInt, IntValue: 1024},
}
fill := clikart.ScalarOrTensor{Kind: clikart.ScalarOrTensorDouble, DoubleValue: value}
on := clikart.StreamOrDevice{Kind: clikart.StreamOrDeviceStream, Stream: s}
return must(api.OpFull(shape, fill, clikart.Float32, on, clikart.StreamOrDevice{}))
}
func main() {
api := must(clikart.Load("libClikaRT.so"))
yesNo := map[bool]string{true: "yes", false: "no"}
// Ops on a NON-default stream run on a worker, not inline on this thread.
worker := must(api.StreamCreate(api.DeviceCpu(0)))
a := fullOn(api, worker, 0.001)
w := fullOn(api, worker, 0.001)
check(api.StreamSynchronize(worker)) // settle the inputs
t0 := time.Now()
x := a
for i := 0; i < 24; i++ {
x = must(clikart.Matmul(x, w))
}
dispatchMs := float64(time.Since(t0).Microseconds()) / 1e3
// The chain carried no placement, so it rode the calling thread's
// asynchronous lane; the result names that stream (an owned reference).
lane := api.TensorStream(x)
fmt.Printf("dispatch returned after %.1f ms, stream idle: %s\n",
dispatchMs, yesNo[api.StreamQueryIdle(lane)])
check(api.StreamSynchronize(lane))
fmt.Printf("ready after %.1f ms, stream idle: %s\n",
float64(time.Since(t0).Microseconds())/1e3, yesNo[api.StreamQueryIdle(lane)])
lane.Release()
// A host read needs none of the above; it waits on its producer alone.
m := must(clikart.Amax(x, nil, false))
fmt.Printf("max(x) = %g\n", *(*float32)(must(api.TensorConstDataPtr(m))))
}
use std::time::Instant;
use clika_rt::{sys, Api};
const F32: sys::clika_rt_data_type = sys::clika_rt_data_type_CLIKA_RT_DATA_TYPE_FLOAT32;
const IDENTITY: sys::clika_rt_activation = sys::clika_rt_activation_CLIKA_RT_ACTIVATION_IDENTITY;
fn value_slot(v: f64) -> sys::clika_rt_scalar_or_tensor {
sys::clika_rt_scalar_or_tensor {
kind: sys::clika_rt_scalar_or_tensor_kind_CLIKA_RT_SCALAR_OR_TENSOR_DOUBLE as i32,
double_value: v, int_value: 0, tensor: std::ptr::null_mut(),
}
}
fn default_placement() -> sys::clika_rt_stream_or_device {
sys::clika_rt_stream_or_device {
kind: sys::clika_rt_stream_or_device_kind_CLIKA_RT_STREAM_OR_DEVICE_DEFAULT as i32,
device: sys::clika_rt_device { api: 0, index: 0 },
stream: std::ptr::null_mut(),
}
}
// full({1024, 1024}, value) on the worker: allocate ON the stream, fill in
// place (the fill runs where its target lives).
fn full_on<'a>(api: &'a Api, worker: &clika_rt::Stream<'_>,
value: f64) -> clika_rt::Result<clika_rt::Tensor<'a>> {
let t = api.f_stream_allocate(worker, &[1024, 1024], F32)?;
api.f_op_fill_inplace(&t, value_slot(value), default_placement())?;
Ok(t)
}
fn main() -> clika_rt::Result<()> {
let api = Api::load("libClikaRT.so")?;
// Ops on a NON-default stream run on a worker, not inline on this thread.
let worker = api.f_stream_create(api.device_cpu(0))?;
let a = full_on(&api, &worker, 0.001)?;
let w = full_on(&api, &worker, 0.001)?;
api.f_stream_synchronize(&worker)?; // settle the inputs
let t0 = Instant::now();
let mut x = a;
for _ in 0..24 {
// matmul consumes both operands; the clone funds w's next use.
x = api.f_op_matmul(x, w.shallow_clone(), api.absent(),
false, IDENTITY, false, false, 0.0, 0.0)?;
}
let dispatch_ms = t0.elapsed().as_secs_f64() * 1e3;
// The chain carried no placement, so it rode the calling thread's
// asynchronous lane; the result names that stream.
let lane = api.tensor_stream(&x).expect("a produced tensor names its stream");
println!("dispatch returned after {dispatch_ms:.1} ms, stream idle: {}",
if api.stream_query_idle(&lane) { "yes" } else { "no" });
api.f_stream_synchronize(&lane)?;
println!("ready after {:.1} ms, stream idle: {}",
t0.elapsed().as_secs_f64() * 1e3,
if api.stream_query_idle(&lane) { "yes" } else { "no" });
// A host read needs none of the above; it waits on its producer alone.
let m = api.f_op_amax(x, &[], false)?;
println!("max(x) = {}", api.tensor_to_string(&m)?);
Ok(())
}
dispatch returned after 23.5 ms, stream idle: no
ready after 24.2 ms, stream idle: yes
max(x) = 0.00176685
The dispatch and the ready walls sit close together on a CPU stream, and the gap between them is the point: dispatch is still not execution (the stream is not idle when the loop returns), but memory on an asynchronous CPU stream follows execution, so a dependent chain is issued one operation ahead of the one executing and the dispatch loop is paced by the work itself. On a CUDA stream the guarantee is the enqueue rather than the run, so the same loop returns in well under a millisecond while the device works behind it. An op placed on a bare Device overlaps with the caller; code that needs it synchronous opts in explicitly, with Stream::default_stream(device) as the placement or a SynchronousStreamScope around the region.
React the moment a result is ready
Tensor::on_complete registers a callback on a result; it fires the moment the producing kernel finishes, on ClikaRT's callback pool while the producer is still running, or inline at registration when the result has already settled. No polling and no blocked thread, the push-style inverse of synchronize(). The tensor's storage is kept alive for the callback, which receives it by const reference. Every binding carries the same member (the Go, Rust and Kotlin tabs), so a consumer in any language reacts the same way.
- C++
- C
- Python
- Kotlin
- Go
- Rust
#include <atomic>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <thread>
#include <ClikaRT/clika_rt.h>
using ClikaRT::DataType;
using ClikaRT::Device;
using ClikaRT::Stream;
using ClikaRT::Tensor;
namespace ops = ClikaRT::ops;
namespace {
double ms_since(std::chrono::steady_clock::time_point t0) {
return std::chrono::duration<double, std::milli>(
std::chrono::steady_clock::now() - t0).count();
}
} // namespace
int main() {
Stream worker = Stream::create(Device::cpu());
const Tensor a = ops::full({1024, 1024}, 0.001, DataType::Float32, worker);
const Tensor w = ops::full({1024, 1024}, 0.001, DataType::Float32, worker);
worker.synchronize();
std::atomic<bool> fired{false};
const auto t0 = std::chrono::steady_clock::now();
Tensor x = a;
for (int i = 0; i < 10; ++i) x = ops::matmul(x, w);
std::printf("main: dispatch done at %.1f ms, registering the callback and doing other work\n",
ms_since(t0));
// Fires the moment the producing kernel finishes: on ClikaRT's callback
// pool while the producer is still running, inline at registration when
// the result has already settled. The storage stays alive for the call.
x.on_complete([&](const Tensor& result) {
std::printf("callback: fired after %.1f ms, x = %s\n",
ms_since(t0), result.to_string().c_str());
fired = true;
});
while (!fired) std::this_thread::yield();
return 0;
}
The api table carries tensor_on_complete: the callback travels as the C callback triple (the function pointer, a userdata pointer passed to it first, and a finalizer the runtime calls exactly once when the registration dies), and the tensor payload is borrowed for the call only. The C++ tab is the shape; the Go and Rust tabs show the same member through their bindings. Waiting where the value is needed remains the simplest form: tensor_synchronize(x) blocks until x's producer has finished, and stream_query_idle polls without blocking (the first sample shows both).
This step is part of the C++ API today; the C++ tab shows it.
import io.clika.runtime.ClikaRtGen
import io.clika.runtime.TensorCallback
import io.clika.runtime.summary
import java.util.concurrent.atomic.AtomicBoolean
// The helper this section shares with DispatchVsReady.kt: full({1024, 1024},
// value) placed on a stream.
fun fullOn(stream: Long, value: Double): Long =
ClikaRtGen.tensorFull(longArrayOf(1024, 1024), value,
ClikaRtGen.DATA_TYPE_FLOAT32,
ClikaRtGen.STREAM_OR_DEVICE_STREAM, 0, 0, stream,
ClikaRtGen.STREAM_OR_DEVICE_DEFAULT, 0, 0, 0L)
fun main() {
ClikaRtGen.load()
val cpu = ClikaRtGen.deviceCpu(0)
val worker = ClikaRtGen.streamCreate(
ClikaRtGen.deviceApiOf(cpu), ClikaRtGen.deviceIndexOf(cpu))
val a = fullOn(worker, 0.001)
val w = fullOn(worker, 0.001)
ClikaRtGen.streamSynchronize(worker)
val fired = AtomicBoolean(false)
val t0 = System.nanoTime()
var x = a
repeat(10) {
ClikaRtGen.tensorRetain(w) // matmul consumes both operands
x = ClikaRtGen.opMatmul(x, w, 0L, false,
ClikaRtGen.ACTIVATION_IDENTITY, false, false, 0.0, 0.0)
}
println("main: dispatch done at %.1f ms, registering the callback and doing other work"
.format((System.nanoTime() - t0) / 1e6))
// Fires the moment the producer finishes: on ClikaRT's callback pool while
// the producer is still running, inline at registration when the result
// has already settled. The Tensor handed in is the callback's for the call only.
ClikaRtGen.tensorOnComplete(x, TensorCallback { result ->
println("callback: fired after %.1f ms, x = %s"
.format((System.nanoTime() - t0) / 1e6, result.summary()))
fired.set(true)
})
while (!fired.get()) {
Thread.yield()
}
ClikaRtGen.tensorRelease(x)
ClikaRtGen.tensorRelease(w)
ClikaRtGen.streamRelease(worker)
}
package main
import (
"fmt"
"log"
"runtime"
"sync/atomic"
"time"
"github.com/Clika/clika_runtime/bindings/go/clikart"
)
func must[T any](v T, err error) T {
if err != nil {
log.Fatal(err)
}
return v
}
func check(err error) {
if err != nil {
log.Fatal(err)
}
}
func main() {
api := must(clikart.Load("libClikaRT.so"))
worker := must(api.StreamCreate(api.DeviceCpu(0)))
a := fullOn(api, worker, 0.001) // the helper from the first sample
w := fullOn(api, worker, 0.001)
check(api.StreamSynchronize(worker))
var fired atomic.Bool
t0 := time.Now()
x := a
for i := 0; i < 10; i++ {
x = must(clikart.Matmul(x, w))
}
fmt.Printf("main: dispatch done at %.1f ms, registering the callback and doing other work\n",
float64(time.Since(t0).Microseconds())/1e3)
// Fires the moment the producer finishes: on ClikaRT's callback pool while
// the producer is still running, inline at registration when the result has
// already settled. The payload is an owned handle for the call's duration
// (Retain to keep it).
check(api.TensorOnComplete(x, func(result *clikart.Tensor) {
fmt.Printf("callback: fired after %.1f ms, x = %s\n",
float64(time.Since(t0).Microseconds())/1e3, result)
fired.Store(true)
}))
for !fired.Load() {
runtime.Gosched()
}
}
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Instant;
use clika_rt::{sys, Api, Tensor};
const F32: sys::clika_rt_data_type = sys::clika_rt_data_type_CLIKA_RT_DATA_TYPE_FLOAT32;
const IDENTITY: sys::clika_rt_activation = sys::clika_rt_activation_CLIKA_RT_ACTIVATION_IDENTITY;
fn value_slot(v: f64) -> sys::clika_rt_scalar_or_tensor {
sys::clika_rt_scalar_or_tensor {
kind: sys::clika_rt_scalar_or_tensor_kind_CLIKA_RT_SCALAR_OR_TENSOR_DOUBLE as i32,
double_value: v, int_value: 0, tensor: std::ptr::null_mut(),
}
}
fn default_placement() -> sys::clika_rt_stream_or_device {
sys::clika_rt_stream_or_device {
kind: sys::clika_rt_stream_or_device_kind_CLIKA_RT_STREAM_OR_DEVICE_DEFAULT as i32,
device: sys::clika_rt_device { api: 0, index: 0 },
stream: std::ptr::null_mut(),
}
}
// The helper this section shares with dispatch_vs_ready.rs: full({1024, 1024},
// value) on the worker, allocated ON the stream and filled in place.
fn full_on<'a>(api: &'a Api, worker: &clika_rt::Stream<'_>,
value: f64) -> clika_rt::Result<Tensor<'a>> {
let t = api.f_stream_allocate(worker, &[1024, 1024], F32)?;
api.f_op_fill_inplace(&t, value_slot(value), default_placement())?;
Ok(t)
}
fn main() -> clika_rt::Result<()> {
let api = Api::load("libClikaRT.so")?;
let worker = api.f_stream_create(api.device_cpu(0))?;
let a = full_on(&api, &worker, 0.001)?;
let w = full_on(&api, &worker, 0.001)?;
api.f_stream_synchronize(&worker)?;
let fired = Arc::new(AtomicBool::new(false));
let t0 = Instant::now();
let mut x = a;
for _ in 0..10 {
x = api.f_op_matmul(x, w.shallow_clone(), api.absent(),
false, IDENTITY, false, false, 0.0, 0.0)?;
}
println!("main: dispatch done at {:.1} ms, registering the callback and doing other work",
t0.elapsed().as_secs_f64() * 1e3);
// Fires the moment the producer finishes: on ClikaRT's callback pool while
// the producer is still running, inline at registration when the result
// has already settled. The closure owns its own handle to the runtime
// (clone_handle), so it can read the result whatever the registering code
// is doing by then; the payload is an owned handle for the call's duration.
let fired_cb = Arc::clone(&fired);
let api_cb = api.clone_handle();
api.f_tensor_on_complete(&x, Some(Box::new(move |result: Tensor<'_>| {
println!("callback: fired after {:.1} ms, x = {}",
t0.elapsed().as_secs_f64() * 1e3,
api_cb.tensor_to_string(&result).unwrap_or_default());
fired_cb.store(true, Ordering::Release);
})))?;
while !fired.load(Ordering::Acquire) {
std::thread::yield_now();
}
Ok(())
}
main: dispatch done at 12.1 ms, registering the callback and doing other work
callback: fired after 13.0 ms, x = Tensor(shape=[1024, 1024], dtype=Float32, device=CPU, numel=1048576, data=[0.001268, 0.001268, 0.001268, 0.001268, 0.001268, 0.001268, ...])
Use it to hand results to a queue, complete a request, or chain host-side work without dedicating a thread to waiting. Keep callbacks short; they share the callback pool.
Step synchronously while debugging
SynchronousStreamScope is RAII: inside it, every op the calling thread dispatches to the stream completes before the call returns, so the program state after each line is exactly what the line computed. Deterministic and slow, which is the right trade while hunting a numeric bug or stepping in a debugger. Open it on a quiescent stream, and pipelining resumes when the scope closes. It is also the region-sized opt-in for code that needs synchronous bare-Device behavior.
- C++
- C
- Python
- Kotlin
- Go
- Rust
#include <cstdint>
#include <cstdio>
#include <ClikaRT/clika_rt.h>
using ClikaRT::DataType;
using ClikaRT::Device;
using ClikaRT::Stream;
using ClikaRT::SynchronousStreamScope;
using ClikaRT::Tensor;
namespace ops = ClikaRT::ops;
int main() {
Stream worker = Stream::create(Device::cpu());
const Tensor a = ops::full({256, 256}, 0.001, DataType::Float32, worker);
const Tensor w = ops::full({256, 256}, 0.001, DataType::Float32, worker);
worker.synchronize();
// An op with no placement rides the calling thread's asynchronous lane;
// its result names that stream, which is the one to query.
Tensor x = ops::matmul(a, w); // async: dispatched, likely still running
std::printf("no scope : idle after dispatch: %s\n",
x.stream().query_idle() ? "yes" : "no");
x.stream().synchronize();
{
SynchronousStreamScope scope(worker);
x = ops::matmul(a, w); // completes before this line returns
std::printf("sync scope : idle after dispatch: %s\n",
x.stream().query_idle() ? "yes" : "no");
}
return 0;
}
#include <stdio.h>
#include "clika_rt/clika_rt_core.h"
extern const clika_rt_api* api;
extern void check(clika_rt_error* e, const char* where);
extern clika_rt_tensor* full_on(clika_rt_stream* s, double value); /* 256x256 here */
int main(void) {
clika_rt_stream* worker = NULL;
check(api->stream_create(api->device_cpu(0), &worker), "stream_create");
clika_rt_tensor* a = full_on(worker, 0.001);
clika_rt_tensor* w = full_on(worker, 0.001);
check(api->stream_synchronize(worker), "sync");
/* Both matmuls consume both operands: fund the second call up front. */
api->tensor_retain(a);
api->tensor_retain(w);
clika_rt_tensor* x = NULL; /* async: dispatched, likely still running */
check(api->op_matmul(a, w, NULL, 0, CLIKA_RT_ACTIVATION_IDENTITY,
0, 0, 0.0, 0.0, &x), "matmul");
/* An op with no placement rides the calling thread's asynchronous lane;
* its result names that stream (an owned reference), the one to query. */
clika_rt_stream* lane = api->tensor_stream(x);
printf("no scope : idle after dispatch: %s\n",
api->stream_query_idle(lane) ? "yes" : "no");
check(api->stream_synchronize(lane), "sync");
api->stream_release(lane);
api->tensor_release(x);
/* The RAII scope is a C++ tier convenience; at this surface synchronous
* stepping is an explicit synchronize of the result's stream after each
* dispatch. */
check(api->op_matmul(a, w, NULL, 0, CLIKA_RT_ACTIVATION_IDENTITY,
0, 0, 0.0, 0.0, &x), "matmul");
lane = api->tensor_stream(x);
check(api->stream_synchronize(lane), "sync"); /* completes here */
printf("stepped : idle after dispatch+sync: %s\n",
api->stream_query_idle(lane) ? "yes" : "no");
api->stream_release(lane);
api->tensor_release(x);
api->stream_release(worker);
return 0;
}
This step is part of the C++ API today; the C++ tab shows it.
import io.clika.runtime.ClikaRtGen
fun fullOn(stream: Long, value: Double): Long =
ClikaRtGen.tensorFull(longArrayOf(256, 256), value,
ClikaRtGen.DATA_TYPE_FLOAT32,
ClikaRtGen.STREAM_OR_DEVICE_STREAM, 0, 0, stream,
ClikaRtGen.STREAM_OR_DEVICE_DEFAULT, 0, 0, 0L)
fun main() {
ClikaRtGen.load()
val cpu = ClikaRtGen.deviceCpu(0)
val worker = ClikaRtGen.streamCreate(
ClikaRtGen.deviceApiOf(cpu), ClikaRtGen.deviceIndexOf(cpu))
val a = fullOn(worker, 0.001)
val w = fullOn(worker, 0.001)
ClikaRtGen.streamSynchronize(worker)
// Both matmuls consume both operands: fund the second call up front.
ClikaRtGen.tensorRetain(a)
ClikaRtGen.tensorRetain(w)
// async: dispatched, likely still running
var x = ClikaRtGen.opMatmul(a, w, 0L, false,
ClikaRtGen.ACTIVATION_IDENTITY, false, false, 0.0, 0.0)
// An op with no placement rides the calling thread's asynchronous lane;
// its result names that stream (an owned handle), the one to query.
var lane = ClikaRtGen.tensorStream(x)
println("no scope : idle after dispatch: " +
if (ClikaRtGen.streamQueryIdle(lane)) "yes" else "no")
ClikaRtGen.streamSynchronize(lane)
ClikaRtGen.streamRelease(lane)
ClikaRtGen.tensorRelease(x)
// The RAII scope is a C++ tier convenience; at this surface synchronous
// stepping is an explicit synchronize of the result's stream after each
// dispatch.
x = ClikaRtGen.opMatmul(a, w, 0L, false,
ClikaRtGen.ACTIVATION_IDENTITY, false, false, 0.0, 0.0)
lane = ClikaRtGen.tensorStream(x)
ClikaRtGen.streamSynchronize(lane) // completes here
println("stepped : idle after dispatch+sync: " +
if (ClikaRtGen.streamQueryIdle(lane)) "yes" else "no")
ClikaRtGen.streamRelease(lane)
ClikaRtGen.tensorRelease(x)
ClikaRtGen.streamRelease(worker)
}
package main
import (
"fmt"
"log"
"github.com/Clika/clika_runtime/bindings/go/clikart"
)
func must[T any](v T, err error) T {
if err != nil {
log.Fatal(err)
}
return v
}
func check(err error) {
if err != nil {
log.Fatal(err)
}
}
func main() {
api := must(clikart.Load("libClikaRT.so"))
yesNo := map[bool]string{true: "yes", false: "no"}
worker := must(api.StreamCreate(api.DeviceCpu(0)))
a := fullOn(api, worker, 0.001) // 256x256 in this sample
w := fullOn(api, worker, 0.001)
check(api.StreamSynchronize(worker))
// An op with no placement rides the calling thread's asynchronous lane;
// its result names that stream (an owned reference), the one to query.
x := must(clikart.Matmul(a, w)) // async: dispatched, likely still running
lane := api.TensorStream(x)
fmt.Printf("no scope : idle after dispatch: %s\n", yesNo[api.StreamQueryIdle(lane)])
check(api.StreamSynchronize(lane))
lane.Release()
scope := must(api.SynchronousStreamScopeOpen(worker))
_ = must(clikart.Matmul(a, w)) // completes before this call returns
fmt.Printf("sync scope : idle after dispatch: %s\n", yesNo[api.StreamQueryIdle(worker)])
api.SynchronousStreamScopeClose(scope)
}
use clika_rt::{sys, Api, Tensor};
const F32: sys::clika_rt_data_type = sys::clika_rt_data_type_CLIKA_RT_DATA_TYPE_FLOAT32;
const IDENTITY: sys::clika_rt_activation = sys::clika_rt_activation_CLIKA_RT_ACTIVATION_IDENTITY;
fn value_slot(v: f64) -> sys::clika_rt_scalar_or_tensor {
sys::clika_rt_scalar_or_tensor {
kind: sys::clika_rt_scalar_or_tensor_kind_CLIKA_RT_SCALAR_OR_TENSOR_DOUBLE as i32,
double_value: v, int_value: 0, tensor: std::ptr::null_mut(),
}
}
fn default_placement() -> sys::clika_rt_stream_or_device {
sys::clika_rt_stream_or_device {
kind: sys::clika_rt_stream_or_device_kind_CLIKA_RT_STREAM_OR_DEVICE_DEFAULT as i32,
device: sys::clika_rt_device { api: 0, index: 0 },
stream: std::ptr::null_mut(),
}
}
// The helper this section shares with dispatch_vs_ready.rs, at this section's
// size: full({256, 256}, value) on the worker.
fn full_on<'a>(api: &'a Api, worker: &clika_rt::Stream<'_>,
value: f64) -> clika_rt::Result<Tensor<'a>> {
let t = api.f_stream_allocate(worker, &[256, 256], F32)?;
api.f_op_fill_inplace(&t, value_slot(value), default_placement())?;
Ok(t)
}
// matmul consumes both operands; the clones fund the caller's handles.
fn matmul<'a>(api: &'a Api, a: &Tensor<'_>, w: &Tensor<'_>) -> clika_rt::Result<Tensor<'a>> {
api.f_op_matmul(a.shallow_clone(), w.shallow_clone(), api.absent(),
false, IDENTITY, false, false, 0.0, 0.0)
}
fn main() -> clika_rt::Result<()> {
let api = Api::load("libClikaRT.so")?;
let worker = api.f_stream_create(api.device_cpu(0))?;
let a = full_on(&api, &worker, 0.001)?;
let w = full_on(&api, &worker, 0.001)?;
api.f_stream_synchronize(&worker)?;
// async: dispatched, likely still running. An op with no placement rides
// the calling thread's asynchronous lane; its result names that stream.
let x = matmul(&api, &a, &w)?;
let lane = api.tensor_stream(&x).expect("a produced tensor names its stream");
println!("no scope : idle after dispatch: {}",
if api.stream_query_idle(&lane) { "yes" } else { "no" });
api.f_stream_synchronize(&lane)?;
{
let _scope = api.f_synchronous_stream_scope(&worker)?; // drop guard
let x = matmul(&api, &a, &w)?; // completes before this line returns
let lane = api.tensor_stream(&x).expect("a produced tensor names its stream");
println!("sync scope : idle after dispatch: {}",
if api.stream_query_idle(&lane) { "yes" } else { "no" });
}
Ok(())
}
no scope : idle after dispatch: no
sync scope : idle after dispatch: yes
Build the whole graph first, run it once
TracingScope flips the calling thread the other way, to lazy: ops return Unscheduled placeholder tensors carrying lineage and no kernel runs. One synchronize() (or any read) materializes the graph leaf-first. Use it to declare a computation in full before spending anything, or to hand the runtime the widest possible scheduling view. The same idea with a reusable artifact is tracing eager code to a graph: capture a function once as a ModelGraph and run it repeatedly, instead of scoping one thread's dispatches.
- C++
- C
- Python
- Kotlin
- Go
- Rust
#include <cstdint>
#include <cstdio>
#include <ClikaRT/clika_rt.h>
using ClikaRT::DataType;
using ClikaRT::Device;
using ClikaRT::Tensor;
using TensorStatus = ClikaRT::Tensor::Status;
using ClikaRT::TracingScope;
namespace ops = ClikaRT::ops;
namespace {
const char* name(TensorStatus s) {
switch (s) {
case TensorStatus::Unscheduled: return "Unscheduled";
case TensorStatus::Evaluated: return "Evaluated";
case TensorStatus::Available: return "Available";
}
return "?";
}
} // namespace
int main() {
const Tensor a = ops::ones({4, 4}, DataType::Float32, Device::cpu());
const Tensor b = ops::ones({4, 4}, DataType::Float32, Device::cpu());
Tensor m, s;
{
TracingScope trace;
m = ops::matmul(a, b); // no kernel runs
s = ops::add(m, a);
std::printf("traced : m=%s s=%s\n", name(m.status()), name(s.status()));
s.synchronize(); // materialize the graph, leaf-first
}
// Outside the scope, ops run eagerly again: a one-element view of the
// realized result and its value readback.
std::printf("realized: m=%s s=%s, s[0] = %g (4 ones dot ones + 1 = 5)\n",
name(m.status()), name(s.status()),
ops::select(s.reshape({-1}), 0, 0).item<float>());
return 0;
}
The api table carries the scopes: tracing_scope_open returns a handle bound to the calling thread, and tracing_scope_close on the same thread restores the mode the thread had (eager_scope_open and synchronous_stream_scope_open follow the same open-and-close pair). The Go and Rust tabs show the pair through their bindings. The table's other deferral is the graph capture: trace_begin hands back data-free stand-ins and records the ops called on them, and trace_end returns a ModelGraph to run when you choose (tracing eager code to a graph walks it).
This step is part of the C++ API today; the C++ tab shows it.
import io.clika.runtime.ClikaRtGen
fun ones4x4(): Long =
ClikaRtGen.tensorOnes(longArrayOf(4, 4), ClikaRtGen.DATA_TYPE_FLOAT32,
ClikaRtGen.STREAM_OR_DEVICE_DEFAULT, 0, 0, 0L,
ClikaRtGen.STREAM_OR_DEVICE_DEFAULT, 0, 0, 0L)
fun statusName(status: Int): String = when (status) {
ClikaRtGen.TENSOR_STATUS_UNSCHEDULED -> "unscheduled"
ClikaRtGen.TENSOR_STATUS_EVALUATED -> "evaluated"
ClikaRtGen.TENSOR_STATUS_AVAILABLE -> "available"
else -> "status$status"
}
fun main() {
ClikaRtGen.load()
val a = ones4x4()
val b = ones4x4()
// Inside the scope this thread records ops symbolically; no kernel runs
// until a synchronize materializes the graph. Close on the same thread.
val trace = ClikaRtGen.tracingScopeOpen()
ClikaRtGen.tensorRetain(a) // matmul and add each consume their operands
ClikaRtGen.tensorRetain(b)
val m = ClikaRtGen.opMatmul(a, b, 0L, false,
ClikaRtGen.ACTIVATION_IDENTITY, false, false, 0.0, 0.0) // no kernel runs
ClikaRtGen.tensorRetain(m)
val s = ClikaRtGen.opAddTensor(m, ClikaRtGen.SCALAR_OR_TENSOR_TENSOR,
0.0, 0L, a, 1.0, ClikaRtGen.ACTIVATION_IDENTITY)
println("traced : m=${statusName(ClikaRtGen.tensorStatus(m))} " +
"s=${statusName(ClikaRtGen.tensorStatus(s))}")
// A flat view of s, also traced; synchronizing it materializes the whole
// graph leaf-first, and the host read then sees real values.
ClikaRtGen.tensorRetain(s)
val flat = ClikaRtGen.opFlatten(s, 0, -1)
ClikaRtGen.tensorSynchronize(flat)
val first = ClikaRtGen.tensorToVecF32(flat)[0]
println("realized: m=${statusName(ClikaRtGen.tensorStatus(m))} " +
"s=${statusName(ClikaRtGen.tensorStatus(s))}, " +
"s[0] = $first (4 ones dot ones + 1 = 5)")
ClikaRtGen.tracingScopeClose(trace)
ClikaRtGen.tensorRelease(flat)
ClikaRtGen.tensorRelease(s)
ClikaRtGen.tensorRelease(m)
ClikaRtGen.tensorRelease(b)
}
package main
import (
"fmt"
"log"
"github.com/Clika/clika_runtime/bindings/go/clikart"
)
func must[T any](v T, err error) T {
if err != nil {
log.Fatal(err)
}
return v
}
func check(err error) {
if err != nil {
log.Fatal(err)
}
}
func name(s clikart.TensorStatus) string {
switch s {
case clikart.TensorStatusUnscheduled:
return "Unscheduled"
case clikart.TensorStatusEvaluated:
return "Evaluated"
case clikart.TensorStatusAvailable:
return "Available"
}
return "?"
}
func main() {
api := must(clikart.Load("libClikaRT.so"))
a := must(api.TensorOnes([]int64{4, 4}, clikart.Float32,
clikart.StreamOrDevice{}, clikart.StreamOrDevice{}))
b := must(api.TensorOnes([]int64{4, 4}, clikart.Float32,
clikart.StreamOrDevice{}, clikart.StreamOrDevice{}))
trace := must(api.TracingScopeOpen())
m := must(clikart.Matmul(a, b)) // no kernel runs
s := must(m.AddTensor(clikart.ScalarOrTensor{
Kind: clikart.ScalarOrTensorTensor, Tensor: a,
}, 1.0, clikart.ActivationIdentity))
fmt.Printf("traced : m=%s s=%s\n", name(api.TensorStatus(m)), name(api.TensorStatus(s)))
check(api.TensorSynchronize(s)) // materialize the graph, leaf-first
first := *(*float32)(must(api.TensorConstDataPtr(s)))
fmt.Printf("realized: m=%s s=%s, s[0] = %g (4 ones dot ones + 1 = 5)\n",
name(api.TensorStatus(m)), name(api.TensorStatus(s)), first)
api.TracingScopeClose(trace)
}
use clika_rt::{sys, Api, IntoOperand};
const F32: sys::clika_rt_data_type = sys::clika_rt_data_type_CLIKA_RT_DATA_TYPE_FLOAT32;
const IDENTITY: sys::clika_rt_activation = sys::clika_rt_activation_CLIKA_RT_ACTIVATION_IDENTITY;
fn default_placement() -> sys::clika_rt_stream_or_device {
sys::clika_rt_stream_or_device {
kind: sys::clika_rt_stream_or_device_kind_CLIKA_RT_STREAM_OR_DEVICE_DEFAULT as i32,
device: sys::clika_rt_device { api: 0, index: 0 },
stream: std::ptr::null_mut(),
}
}
fn name(s: sys::clika_rt_tensor_status) -> &'static str {
match s {
sys::clika_rt_tensor_status_CLIKA_RT_TENSOR_STATUS_UNSCHEDULED => "Unscheduled",
sys::clika_rt_tensor_status_CLIKA_RT_TENSOR_STATUS_EVALUATED => "Evaluated",
sys::clika_rt_tensor_status_CLIKA_RT_TENSOR_STATUS_AVAILABLE => "Available",
_ => "?",
}
}
fn flat() -> sys::clika_rt_index_bound {
sys::clika_rt_index_bound {
kind: sys::clika_rt_index_bound_kind_CLIKA_RT_INDEX_BOUND_INT as i32,
int_value: -1, tensor: std::ptr::null_mut(),
}
}
fn main() -> clika_rt::Result<()> {
let api = Api::load("libClikaRT.so")?;
let a = api.f_tensor_ones(&[4, 4], F32, default_placement(), default_placement())?;
let b = api.f_tensor_ones(&[4, 4], F32, default_placement(), default_placement())?;
let (m, s) = {
let _trace = api.f_tracing_scope()?; // drop guard
let m = api.f_op_matmul(a.shallow_clone(), b.shallow_clone(), api.absent(),
false, IDENTITY, false, false, 0.0, 0.0)?; // no kernel runs
let s = api.f_op_add_tensor(m.shallow_clone(), (&a).into_operand(), 1.0, IDENTITY)?;
println!("traced : m={} s={}", name(api.tensor_status(&m)), name(api.tensor_status(&s)));
api.f_tensor_synchronize(&s)?; // materialize the graph, leaf-first
(m, s)
};
// Outside the scope, ops run eagerly again: a flat view of the realized
// result and its value readback.
let first = api.f_tensor_to_vec_f32(&api.f_tensor_reshape(&s, &[flat()])?)?[0];
println!("realized: m={} s={}, s[0] = {first} (4 ones dot ones + 1 = 5)",
name(api.tensor_status(&m)), name(api.tensor_status(&s)));
Ok(())
}
traced : m=Unscheduled s=Unscheduled
realized: m=Evaluated s=Evaluated, s[0] = 5 (4 ones dot ones + 1 = 5)
The four tools compose into one rule of thumb: leave the asynchronous default alone for throughput, read results and let the reads wait, reach for on_complete when a thread should not wait, and reserve the two scopes for debugging (synchronous) and up-front graph building (tracing). The bundle's async example walks each in its own chapter, including safe-reads patterns this guide leaves implicit.