Phase 1, Section 4: Observability — Tracing, Logging, Diagnostics
Phase 1: Implementation — the shape-typed core compiled end-to-end. Section 4 of 8: Observability — Tracing, Logging, Diagnostics.
1. What this section commits to
The instrumentation infrastructure that makes Phase 1 debuggable from the first day rather than from the first crisis. Specifically:
- Three pillars: tracing (what happened, in order, with timing), logging (records of events with context), diagnostics (helping the user understand).
- Tooling commitments:
Logsfor OCaml,tracing+tracing-subscriberfor Rust,nvtx-rsfor NVIDIA NVTX integration, JSON-lines output as the structured format. - Cross-system correlation: a module ID propagating from compiler to runtime so a single trace shows compilation → linkage → execution as one continuous timeline.
- Performance discipline: zero-cost when disabled at compile time; near-zero when disabled at runtime; explicitly not present in hot paths (per-element kernel loops).
- Specific additions Sections 2 and 3 need to incorporate.
This section retrofits a gap. Sections 2 and 3 didn't commit to observability infrastructure beyond basic error handling. This section corrects that, with explicit notes on what each earlier section needs to gain.
2. Why this belongs in Phase 1, not later
The discipline is the same as for the runtime FFI design (Section 3 §6.1) and the DLPack ABI prefix (Section 3 §5): architectural commitments that are cheap to make now and expensive to retrofit later.
Concrete consequences of not having observability from day one:
- Bugs are diagnosed by
printfdebugging, which doesn't compose across compiler/runtime boundaries. - Performance regressions are invisible until users complain; by then, the regression has been baked into multiple commits.
- Memory leaks require special-purpose instrumentation each time; the
LoggingMemoryResource(Section 3 §4.2) needs siblings for kernel launches, stream operations, FFI calls. - New operations added in Section 5 land without instrumentation; instrumenting them later is per-operation work that compounds.
Concrete consequences of having observability from day one:
- Every Phase 1 acceptance test produces a trace; failing tests have actionable trace context.
- The performance regression infrastructure (Section 7) consumes the same traces; no separate benchmarking instrumentation needed.
- The bootstrap-and-self-modeling forward track's AD-as-handler PoC (when it lands in Phase 2) inherits compiler/runtime instrumentation; debugging the handler is debugging the trace.
- The Arrow / observability forward track's eventual landing has a head start: the compiler and runtime already emit structured traces in a format compatible with the OTel ecosystem.
The cost of building this in from the start is small — a few weeks of milestone work spread across Phase 1's six months. The cost of retrofitting it later is much larger and tends to be lossy (some events happened too long ago to instrument retroactively).
3. The three pillars
3.1 Tracing
What: span-based timeline of operations. Each span has a name, a duration, a parent (forming a tree), and a set of attributes (key-value pairs).
Granularity:
- Compiler: one span per compilation phase (parse, elaborate, typecheck, lower, emit, link). Sub-spans for SMT discharges, subprocess invocations.
- Runtime: one span per FFI entry point invocation. Sub-spans for memory operations, kernel launches, stream synchronizations.
- Per-element kernel work: no spans. The cost would dominate the work.
Output: structured (JSON-lines or OpenTelemetry-compatible) when configured; human-readable text to stderr by default.
3.2 Logging
What: append-only records of events. Distinct from tracing in that logs don't have duration semantics — they're points in time.
Levels: trace, debug, info, warn, error. Configurable per-module; runtime-tunable via environment variables.
Discipline:
erroralways emitted, never suppressed.warnalways emitted by default.infoemitted by default; suppressible.debugandtraceopt-in.
Format: structured key-value pairs. Not just text strings. Tools that consume logs (a future debugging dashboard, ad-hoc grep-and-jq pipelines, the eventual OTel integration) consume the structure.
3.3 Diagnostics
What: information designed for the user, not for machines. Compile-time error messages with locations and suggestions; runtime errors with context; performance reports; memory reports.
Discipline: every diagnostic answers three questions — what happened, where did it happen, what should the user do about it. The third is the most often skipped and the most valuable.
Section 2 §10 already committed to this for compile-time errors. This section extends it to runtime errors and to performance/memory diagnostics.
4. The compiler side (additions to Section 2)
Section 2 of Phase 1 committed to OCaml + Dune + Z3 + MLIR. Here are the explicit observability additions.
4.1 New library dependency
compiler/lib/
└── observ/
├── dune
├── trace.ml — span management
├── logger.ml — structured logging
└── diagnostics.ml — compile-time diagnostic infrastructure
The observ library wraps Logs (the standard OCaml logging library) for logging, and provides a Trace module for span management. Logs is a Phase 1 dependency added explicitly.
4.2 Trace integration
Every compilation phase wraps its work in a span:
let elaborate ast =
Trace.span "elaborate" (fun () ->
Logger.info "starting elaboration; %d top-level decls" (List.length ast);
let result = Elab.process ast in
Logger.info "elaboration complete; %d obligations generated"
(List.length result.obligations);
result
)
Sub-spans nest inside; an SMT discharge for a single obligation produces a span like elaborate.smt_discharge with attributes for the obligation, the result, and the time spent.
4.3 CLI flags
The compiler's CLI (per Section 2 §9) gains observability options:
compiler [options] -o output.so input.sexp
-v, -vv, -vvv increase log verbosity (info, debug, trace)
--quiet errors only
--trace-output=PATH emit JSON-lines spans to PATH
--trace-format=text|json|otlp default text; json for tooling; otlp future
--module-id=ID override the auto-generated module ID
(used for cross-system correlation; see §6)
4.4 Error diagnostic improvements
Section 2 §10 committed to source locations and SMT counterexamples. This section adds three things:
Trace context in errors. Every error captures the trace span it occurred in. When an error message is emitted, the surrounding trace context is shown:
error: refinement obligation refuted --> example.sexp:5:7 ... = trace context: elaborate > typecheck_function "matmul" > elaborate_callWarnings become structured. Phase 0 §3 §11 mentioned warnings would emerge later; this section commits the format. A warning is a
Diagnostic.twithseverity = Warning, with the same fields as errors.A
--diagnostic-formatflag (default: human; alternatives:gnufor IDE integration,jsonfor tooling). Errors and warnings format consistently across modes.
4.5 What Section 2 needs to add
Practically, Section 2's existing commitments need these additions when Section 2 is updated (or when its addenda are written):
- New
observlibrary in the project structure. LogsandTracecalls at every phase boundary.- Error type extended with
trace_context: Trace.span_id. - CLI flags above.
The compiler size budget (Section 2 §12.5) was 15K lines. Adding the observability infrastructure is maybe 500–800 LoC. Within budget.
5. The runtime side (additions to Section 3)
Section 3's runtime got a passing mention of LoggingMemoryResource. This section makes observability comprehensive.
5.1 New crate dependencies
[dependencies]
tracing = "0.1" # the dominant Rust tracing ecosystem
tracing-subscriber = "0.3" # output formatting + filtering
tracing-appender = "0.2" # rolling files, async writes
nvtx = "1" # NVIDIA NVTX integration (or nvtx-rs equivalent)
metrics = "0.22" # counters and gauges
tracing is the canonical Rust framework for structured, async-aware tracing. nvtx provides bindings to NVIDIA's profiling API. metrics covers counter / gauge / histogram types for the regression infrastructure.
5.2 FFI instrumentation
Every public extern "C" function gets #[tracing::instrument]:
#[no_mangle]
#[tracing::instrument(skip(out), fields(m, n, k, batch))]
pub extern "C" fn runtime_gpu_matmul(
out: *mut Tensor,
a: *const Tensor,
b: *const Tensor,
batch: i64,
m: i64,
k: i64,
n: i64,
dtype_code: i32,
) -> FfiErrorCode {
let _enter = nvtx::range_push!("matmul {}x{}x{}", m, n, k);
// ... actual implementation ...
nvtx::range_pop!();
code
}
The tracing::instrument macro creates a span automatically; the skip annotation excludes opaque pointer arguments from being recorded; fields(...) adds the relevant integer arguments. The nvtx::range_push/pop calls provide NVIDIA Nsight Systems / Nsight Compute visibility (see §7).
5.3 Memory allocation tracing
The LoggingMemoryResource from Section 3 §4.2 becomes a TracingMemoryResource:
pub struct TracingMemoryResource<Inner: MemoryResource> {
inner: Inner,
}
impl<Inner: MemoryResource> MemoryResource for TracingMemoryResource<Inner> {
fn allocate(&self, size: usize, stream: &Stream)
-> Result<DevicePtr, AllocError>
{
let span = tracing::trace_span!("alloc", size, stream = ?stream);
let _guard = span.enter();
let ptr = self.inner.allocate(size, stream)?;
metrics::counter!("runtime.alloc.bytes").increment(size as u64);
metrics::counter!("runtime.alloc.count").increment(1);
tracing::trace!(?ptr, "allocated");
Ok(ptr)
}
// ... similar for deallocate ...
}
This wraps any underlying resource. In typical configuration:
let base = CudaAsyncMemoryResource::new(device);
let pooled = PoolMemoryResource::new(base, /* config */);
let traced = TracingMemoryResource::new(pooled);
runtime::set_default_resource(traced);
Three layers: trace wraps pool wraps base. Each adds behavior; the base is the actual driver.
5.4 Stream operation tracing
Stream synchronizations are spans of their own. Cross-stream waits (one stream waiting for an event on another) are recorded with parent-child context, so the trace shows which operation triggered the wait.
5.5 Kernel launch instrumentation
In addition to NVTX (§7), every kernel launch records:
- Kernel name (e.g.,
softmax_f32,gather_f16). - Input shapes (only the symbolic dimensions; per-element data is not recorded).
- Stream handle.
- Approximate cost (cycles or microseconds, when measurable cheaply).
This is the tracing layer that domain libraries built on the runtime will consume — e.g., a profiler showing "you spent 60% of your time in attention_softmax_f16."
5.6 What Section 3 needs to add
- New crate dependencies in
Cargo.toml. tracing::instrumenton every public FFI function.TracingMemoryResourceas the recommended outer wrapper.- NVTX annotations on kernel launches (per §7).
- Initialization: the runtime's
initfunction takes a tracing subscriber configuration.
The runtime size budget (Section 3 §10) was 8K lines. Observability adds maybe 300–500 LoC. Within budget.
6. Cross-system correlation
A compilation produces a shared object (per Section 2). The runtime executes it (per Section 3). Without correlation, traces from compilation and traces from execution are disjoint timelines that the user has to manually reconcile.
The fix: every compilation produces a module ID, embedded in the artifact, propagated through every runtime span.
6.1 The module ID
A 128-bit UUID generated at compilation time (or supplied via --module-id). The compiler:
- Records the ID in its compilation trace as a span attribute.
- Embeds the ID into the emitted shared object as a string literal (e.g.,
static const char* KINA_MODULE_ID = "...";). - Provides a runtime-callable function in the artifact:
extern "C" const char* kina_module_id(void).
The runtime:
- Calls
kina_module_idwhen loading a.so. - Includes the ID as an attribute on every span originating from operations in that module.
A user's trace can be filtered by module ID, joining compilation and execution traces seamlessly:
$ jq 'select(.attributes.module_id == "abc123...")' all-traces.jsonl
Shows only the trace events related to one specific compiled program.
6.2 Multi-module programs
Future programs may link multiple compiled .so files (e.g., a user program plus a domain library plus the standard library). Each has its own module ID. Spans from the runtime tag themselves with the module ID of the function being invoked. Cross-module calls naturally produce spans with different module IDs at the call boundary.
This is a future concern (Phase 5+), but the design is forward-compatible: the module ID is per-call, not per-process.
7. NVTX: NVIDIA-specific instrumentation
The single most valuable tool for debugging GPU work. NVIDIA's NVTX library lets the runtime annotate ranges that NVIDIA Nsight Systems and Nsight Compute display in their profiler UIs.
7.1 What this enables
Without NVTX, profiling a CUDA program shows kernel launches as opaque cuLaunchKernel calls. With NVTX:
- Each kernel launch shows as a named range (
matmul 128x256x64,softmax axis=2). - Memory allocations and copies show as ranges (
cudaMemcpyAsync 4MB H2D). - Application-level operations (a transformer block forward pass) show as parent ranges containing the kernel ranges.
- Stream timelines show parallel work and synchronization explicitly.
For someone debugging a slow kernel, this is the difference between "your program spent 40% of its time in unidentified GPU work" and "your program's softmax kernel is taking 2.3ms when it should take 0.4ms."
7.2 Implementation
The runtime wraps every kernel launch and every memory operation with NVTX ranges:
#[inline]
pub fn launch_kernel<F: FnOnce()>(name: &str, f: F) {
let _range = nvtx::Range::new(name); // RAII; pops at scope exit
f()
}
Used everywhere kernel launches happen:
launch_kernel(&format!("matmul {}x{}x{}", m, n, k), || {
cublas::sgemm_async(/* ... */);
});
The cost is a few hundred nanoseconds per range; negligible compared to kernel time.
7.3 Acceptance criterion
Running a Phase 1 demo program (e.g., the transformer block from Section 1's exit criterion) under Nsight Systems should produce a timeline where every kernel is labeled with a meaningful name. No unknown ranges. No anonymous cuLaunchKernel calls.
8. Output format and consumption
8.1 Default: human-readable text to stderr
[2026-05-08T20:45:12.183Z INFO runtime] starting matmul 128x256x64 on cuda:0
[2026-05-08T20:45:12.184Z DEBUG runtime] allocated 32768 bytes for output
[2026-05-08T20:45:12.187Z INFO runtime] matmul completed in 2.8ms
Format: timestamp, level, module, message. Configurable via RUST_LOG (Rust runtime) and OCAMLRUNPARAM-style env vars (OCaml compiler). Standard practice.
8.2 Structured: JSON-lines
{"ts":"2026-05-08T20:45:12.183Z","level":"INFO","span":"matmul","module_id":"abc123","attributes":{"m":128,"n":256,"k":64,"device":"cuda:0"}}
{"ts":"2026-05-08T20:45:12.184Z","level":"DEBUG","span":"alloc","module_id":"abc123","attributes":{"bytes":32768}}
{"ts":"2026-05-08T20:45:12.187Z","level":"INFO","span":"matmul","module_id":"abc123","duration_ms":2.8,"event":"complete"}
JSON-lines is greppable, jq-able, and trivially convertible to other formats. Used when the consumer is a tool rather than a human.
8.3 Future: OpenTelemetry export
The Arrow / observability forward track flagged this connection. JSON-lines is structurally compatible with OTel; an explicit OTLP exporter is a Phase 5+ deliverable. The Phase 1 commitment is not breaking compatibility — span attribute names follow OTel conventions where applicable (http.*, db.*, etc. are reserved; ours use runtime.*, compiler.*).
8.4 Future: Nsight Systems direct integration
Beyond NVTX, Nsight Systems can ingest custom data formats. A future hook to pipe our structured traces directly into Nsight (rather than just NVTX ranges) would unify CPU and GPU views in one tool. Phase 5+ work; Phase 1 just doesn't preclude it.
9. Performance: zero-cost when disabled
The hard discipline. Observability that costs more than 5% in the default-enabled case is observability that gets disabled in production, which means the production code paths run untraced and undebuggable.
9.1 Compile-time elimination
Both tracing (Rust) and Logs (OCaml) support compile-time level filtering. Logs below the configured threshold compile to nothing — not even a function call. The macros expand to if false { ... } else { /* nothing */ }, which the compiler eliminates.
For our build:
- Debug build: all levels compiled in. Verbose by default.
- Release build with default features:
infoand above compiled in;debugandtraceremoved. - Release build with
--features minimal-tracing: onlywarnanderror. For benchmarking only.
9.2 Runtime filtering
When a level is compiled in but runtime-disabled (the user set RUST_LOG=info and a debug! call fires), the cost is one atomic load + one comparison + one branch — a few nanoseconds.
Tracing spans have a similar cost when no subscriber is attached: a few nanoseconds for the level check, then the span body executes without recording.
9.3 Hot paths: no tracing
Per-element kernel loops do not trace. A kernel that processes 10M elements doesn't add a span per element. It adds a single span at the kernel launch level, with attributes describing the work.
Same for memory copy operations within a kernel (these are hardware-level concerns), stream-internal scheduling (hardware), and any operation invoked from inside another instrumented operation (the parent span captures it).
9.4 Acceptance criterion
The trace overhead in default-enabled mode is < 5% on the transformer-block benchmark (Section 1's exit criterion). The trace overhead in fully-disabled mode is < 1%. Both verified by Section 7's regression infrastructure.
10. Implementation milestones (within Phase 1's three milestones)
The observability work spreads across Phase 1's existing milestones rather than being a separate fourth milestone:
10.1 Milestone 1 (months 1–2): bootstrap
- Compiler: structured logging at phase boundaries via
Logs. - Compiler: trace spans for each phase (parse, elaborate, typecheck, lower, emit).
- Runtime:
tracingintegration for FFI entry points. - Output: human-readable to stderr; verbosity flags working.
- Cross-system correlation IDs designed and embedded.
10.2 Milestone 2 (months 3–4): GPU integration
- Runtime: NVTX integration around kernel launches and memory ops.
- Runtime:
TracingMemoryResourceworking alongsidePoolMemoryResource. - Runtime: stream operation tracing.
- JSON-lines output mode functional.
- Acceptance: Nsight Systems shows our kernels with names.
10.3 Milestone 3 (months 5–6): polish and validation
- Performance regression testing for tracing overhead (per §9.4).
- Documentation: a "debugging guide" walking through how to diagnose representative bugs using these tools.
- Diagnostic improvements based on the bugs actually encountered during Milestones 1–2.
- Integration: traces from a single Phase 1 demo program (compilation through execution) form one coherent timeline.
11. Acceptance criteria specific to observability
| Criterion | Target |
|---|---|
| Every compiler phase emits a span | All five: parse, elaborate, typecheck, lower, emit |
| Every public FFI function instruments | All ~17 entry points from Section 3 §6.1 |
| Cross-system correlation works | Module ID propagates from --module-id through to runtime spans |
| NVTX integration | Nsight Systems shows our kernels with names; no unknown ranges |
| Default-mode trace overhead | < 5% on the transformer-block benchmark |
| Disabled-mode trace overhead | < 1% on the transformer-block benchmark |
| Errors include trace context | Every error message includes the spanning context where it occurred |
| Documented debugging guide | At least one walked-through example bug with the trace artifacts |
| JSON-lines format follows OTel conventions | Span attribute names compatible with OTel semantic conventions |
The goal is operationalized: when Phase 1 ends, debugging a problem in the language is no harder than debugging a comparable problem in PyTorch — and in some respects easier, because we have type-system context that PyTorch doesn't.
12. Open issues introduced by this section
OpenTelemetry export timing. Phase 1 emits OTel-compatible JSON-lines; an actual OTLP exporter that sends spans to an OTel collector is Phase 5+ work. Worth flagging that the gap exists; the Arrow/observability forward track may pull this earlier if the typed-dataframes work needs it.
Trace storage and retention. The default is ephemeral (stderr or rotating file). For long-running workloads, traces accumulate fast. A Phase 5+ concern: integration with proper trace storage (Tempo, Jaeger, ClickHouse) per the observability stack.
Privacy of trace contents. Span attributes can contain shape and dtype information, which is fine. They should not contain tensor values, model parameters, or user inputs. Discipline; needs a code-review checklist item.
Cross-process correlation. When multiple compiled programs run in the same process (or across processes), traces should correlate. Per-call module IDs cover the same-process case. Cross-process correlation needs distributed-tracing primitives (W3C trace-context or similar). Phase 5+.
Compiler trace replay. Captured compiler traces could replay a compilation deterministically for bug reproduction. Useful but not Phase 1; flagged as a debugging-tools direction.
The Logs vs. tracing impedance mismatch. OCaml uses
Logs; Rust usestracing. They're philosophically aligned but have different APIs and output formats. The cross-system correlation papers over this for the user; for contributors, it's a small cognitive cost. Documented in the contribution guide.
13. Updates needed in earlier sections
This section retrofits a gap. To keep the spec internally consistent, three earlier documents need addenda when revisited:
Section 1 (Scope)
The deliverable list (§2 of Section 1) needs three additions:
- Tracing infrastructure (compiler + runtime).
- Structured logging.
- Cross-system correlation IDs.
The compiler size budget (15K LoC) and runtime size budget (8K LoC) accommodate the additions per the analyses in §4.5 and §5.6 of this section.
Section 2 (Compiler)
- New
observlibrary in the project structure (per §4). LogsandTracecalls at every phase boundary.- New CLI flags for verbosity, trace output, trace format.
- Error type extended with trace context.
Section 3 (Runtime)
- New crate dependencies:
tracing,tracing-subscriber,nvtx,metrics. #[tracing::instrument]on every public FFI function.TracingMemoryResourceas the recommended outer wrapper.- NVTX annotations on kernel launches.
These additions don't invalidate the earlier sections' commitments; they add a layer of instrumentation discipline.
14. Section 5 preview
Section 5 (formerly Section 4) covers the operation set extensions identified during Phase 0:
- Variadic-batch matmul (
[..., M, K] @ [..., K, N]). - Gather.
- Softmax as a primitive (lowering decomposes internally).
- repeat_interleave.
Each gets a typing rule, CPU lowering, GPU lowering, and tests. These additions transform Phase 0's "can express forward passes with effort" into "can express forward passes idiomatically."
With Section 4 (this section, observability) and Section 5 (operations) in place, Phase 1's implementation work is fully specified. Sections 6–8 cover verification, testing/ABI, and exit.
End of Phase 1, Section 4.