NURLNURL registrynurl-lang.org →

← onnx

onnx 0.2.0 API

pb.nu

packages/onnx/src/pb.nu — a minimal protobuf wire-format decoder.

Just enough proto3 to read an ONNX model: varints, the four wire types, and length-delimited regions (sub-messages, strings, bytes, packed repeated). Pure NURL over a ( Vec u ) byte buffer — no codegen, no schema; the ONNX message shapes live in model.nu, which drives this by field number.

A PbR is a cursor: the buffer plus the current read position. Decoders advance pos; message parsers loop while pos < end, where end comes from the enclosing length-delimited region (or the buffer length at top level). A sub-message is parsed by a fresh PbR over the same buffer (pb_sub) positioned at the payload — so nested ends never clash.

API

: PbR { ( Vec u ) buf i pos i len }

@ pb_wt_varint → i

Wire types (low 3 bits of a field tag).

@ pb_wt_i64 → i

@ pb_wt_len → i

@ pb_wt_i32 → i

@ pb_new ( Vec u ) buf → *PbR

New reader over a byte buffer (cursor at 0, end = buffer length).

@ pb_sub *PbR r i start i len → *PbR

A reader over the sub-region [start, start+len) of the SAME buffer.

@ pb_pos *PbR r → i

@ pb_set_pos *PbR r i p → v

@ pb_end *PbR r → i

@ pb_more *PbR r → b

@ pb_free *PbR r → v

@ pb_byte *PbR r i off → i

Byte at absolute offset (0 past end).

@ pb_varint *PbR r → i

Read a base-128 varint at the cursor; advance past it.

@ pb_tag *PbR r → i

Field tag → field number / wire type.

@ pb_field i tag → i

@ pb_wire i tag → i

@ pb_blob_start *PbR r → i

Length-delimited: read length, return payload start, advance cursor PAST it. Caller reads [start, start+len) (recover len with pb_sub or by math).

@ pb_submsg *PbR r → *PbR

Read a length-delimited field and return a bounded sub-reader over its payload, advancing the parent cursor past it. The caller loops the sub-reader with pb_more and frees it with pb_free. This is how embedded messages (and packed repeated fields) are descended into.

@ pb_i32 *PbR r → i

Fixed 32-bit little-endian at cursor; advance 4. (float bits / fixed32)

@ pb_string *PbR r → String

Read a length-delimited region as a String.

@ pb_skip *PbR r i wire → v

Skip a field of the given wire type (cursor at the value).

@ pb_read_f32_into *PbR r *u dst i n → v

Read n little-endian f32 values from the byte region at the cursor into the host buffer dst (raw u, 4-byte stride), writing each one's exact 32-bit pattern (no float round-trip). Advances the cursor by n4. Used for TensorProto.raw_data → host weights, ready to upload to the GPU.

& c @ nurl_poke_i32 *u base i idx i val → v

4-byte typed store (runtime.c accessor; runtime.o is always linked).


model.nu

packages/onnx/src/model.nu — ONNX schema parsing over pb.nu.

Decodes the subset of the ONNX protobuf needed for feed-forward inference: ModelProto → GraphProto → { NodeProto[], TensorProto initializers, graph input/output names }. Field numbers are the stable ONNX wire layout. Weights (TensorProto.raw_data, little-endian f32) are read straight into a host buffer ready to upload to the GPU.

API

: OAttr { String name i kind f f i i String s ( Vec i ) ints }

── in-memory graph ─────────────────────────────────────────────── One attribute. Captures the value forms used by the ops we run: f (FLOAT, e.g. alpha/epsilon), i (INT, e.g. group), s (STRING, e.g. auto_pad), and ints (INTS, e.g. kernel_shape/strides). kind is unused (the consumer asks by name + form via node_attr_*).

: ONode

: ONode {
    String op_type
    ( Vec String ) inputs
    ( Vec String ) outputs
    ( Vec OAttr ) attrs
}

: OTensor

: OTensor {
    String name
    ( Vec i ) dims
    i nelem
    i host        // *u as i64 (0 = no data)
}

A tensor: name, shape, element count, and host f32 data (raw 32-bit patterns, 4-byte stride) — or host == 0 for graph value placeholders.

: OGraph

: OGraph {
    ( Vec ONode ) nodes
    ( Vec OTensor ) inits
    String input_name
    String output_name
}

@ streq s a s b → b

String equality as a bool (nurl_str_eq returns i).

@ node_attr_f ONode n s name f dflt → f

── attribute access ──────────────────────────────────────────────

@ node_attr_i ONode n s name i dflt → i

@ node_attr_int_at ONode n s name i k i dflt → i

The k-th element of an INTS attribute (dflt if attr/element absent).

@ node_attr_ints_len ONode n s name → i

@ node_attr_s ONode n s name s dflt → s

A STRING attribute's value (dflt if absent/empty).

@ onnx_parse ( Vec u ) bytes → OGraph

── ModelProto (top level) ────────────────────────────────────────

@ graph_find_init OGraph g s name → i

── lookups ───────────────────────────────────────────────────────


ops.nu

packages/onnx/src/ops.nu — ONNX operators as GPU kernels.

Each op is a CUDA-C kernel compiled once via the gpu package's NVRTC path and launched over the gpu.nu interface. Tensors live on the device as raw CUdeviceptr (i64); shapes are tracked by the executor (runtime.nu).

Op set: Gemm, Relu (dense MLP) + Conv, MaxPool, BatchNormalization, LeakyRelu, and broadcast Mul/Add (CNNs, NCHW) — enough to run a tiny-YOLO-class detector. All launches are 1-D: thread idx → output element; batch N is assumed 1.

API

: Kernels

: Kernels {
    GpuKernel gemm   GpuKernel relu
    GpuKernel conv   GpuKernel pool
    GpuKernel bn     GpuKernel lrelu  GpuKernel elt
    b ok
}

Compiled kernels, built once and reused across nodes.

@ ops_compile Gpu g → Kernels

@ ops_free Kernels ks → v

@ op_gemm Gpu g Kernels ks i adptr i bdptr i cdptr i ydptr i M i N i K f alpha f beta i transB → i

@ op_relu Gpu g Kernels ks i xdptr i ydptr i n → i

@ op_conv Gpu g Kernels ks i xd i wd i bd i yd i Cin i H i W i Cout i kh i kw i OH i OW i ph i pw i sh i sw i hasB → i

@ op_maxpool Gpu g Kernels ks i xd i yd i C i H i W i kh i kw i OH i OW i sh i sw i ph i pw → i

@ op_batchnorm Gpu g Kernels ks i xd i scd i bd i md i vd i yd i C i HW f eps → i

@ op_leakyrelu Gpu g Kernels ks i xd i yd i n f alpha → i

@ op_eltwise Gpu g Kernels ks i xd i bd i yd i n i HW i op i bmode → i


main.nu

packages/onnx/src/main.nu — run an ONNX model on the GPU.

onnx <model.onnx> <input.f32> [expected.f32]

Parses the model (pure-NURL protobuf), uploads weights + input to the GPU, executes the graph (Gemm/Relu kernels via packages/gpu + NVRTC), and prints the output. With an expected.f32 it also checks the result against that reference (e.g. an onnxruntime dump) and sets the exit code.

Build: nurlpkg install onnx (or, from the package root, NURL_STDLIB=<repo> ../../nurl.sh src/main.nu)

API

& c @ nurl_peek_f32 *u base i idx → f

@ load_f32 s path *u pcount → *u

Load a raw little-endian f32 file into a fresh host buffer; returns the buffer and writes the element count through pcount (an i64 cell).

@ print_f f x → v

@ main → i


runtime.nu

packages/onnx/src/runtime.nu — the graph executor.

Walks the ONNX graph in node order (ONNX guarantees topological order), keeping every intermediate tensor resident on the GPU. Initializers and the input are uploaded once; each node dispatches to a GPU kernel in ops.nu; the named output is downloaded at the end. A value map (name → device tensor) threads activations between nodes.

Tensors are N-D (shape vector); the dense path reads dims 0,1 as M,K and the conv path reads NCHW from dims 1,2,3 (batch N is assumed 1).

API

: RTensor { String name i dptr ( Vec i ) shape i nelem }

A device-resident tensor: name, CUdeviceptr (i64), shape, element count.

: Engine { Gpu g Kernels ks ( Vec RTensor ) vals b ok }

@ streq2 s a s b → b

@ ceil_div i a i b → i

@ rt_dim RTensor t i ax → i

@ rt_open i ordinal → *Engine

Open a device and compile the kernels.

@ rt_ok *Engine e → b

@ rt_name *Engine e → s

@ rt_put *Engine e s name i dptr ( Vec i ) shape → v

Register a device tensor under name with an explicit shape vector.

@ rt_find *Engine e s name → i

@ rt_at *Engine e i idx → RTensor

@ rt_load_inits *Engine e OGraph g → v

Upload all graph initializers to the device as RTensors (full shape).

@ rt_alloc_out *Engine e s name ( Vec i ) shape → i

Allocate a fresh device tensor with shape, register under name, return its dptr.

@ rt_gemm *Engine e ONode n → v

── op handlers ───────────────────────────────────────────────────

@ rt_relu *Engine e ONode n → v

@ rt_conv *Engine e ONode n → v

@ rt_maxpool *Engine e ONode n → v

@ rt_batchnorm *Engine e ONode n → v

@ rt_leakyrelu *Engine e ONode n → v

@ rt_eltwise *Engine e ONode n i op → v

Mul (op=0) / Add (op=1) with a broadcast operand. ONNX lets the two inputs appear in either order, so the larger tensor is the data (X) and the smaller is the broadcast operand (B).

@ rt_run_shaped *Engine e OGraph g *u input_host ( Vec i ) shape → RTensor

Run the graph on a host input buffer (raw f32). shape is the input tensor shape (e.g. [1,3,416,416]). Returns the output device tensor.

@ rt_run *Engine e OGraph g *u input_host i in_rows i in_cols → RTensor

Convenience for a 2-D (dense) input.

@ rt_download *Engine e RTensor t → *u

Download a device tensor into a fresh host f32 buffer (caller frees).

@ rt_close *Engine e → v