Authoritative detail lives in SPEC.md §2–§3. This page is the map.
Arrows point from a crate to what it depends on. The graph must stay acyclic. Grey nodes are external.
flowchart BT
core[oxidelake-core<br/>EngineError · BackendKind · BufferLayout · BatchStream · TelemetryHub]
memory[oxidelake-memory<br/>AlignedBuf · pinned/UMA allocators · SpillManager] --> core
device[oxidelake-device<br/>GpuBackend · Cpu/Cuda/Metal · HardwareDetector] --> memory
compute[oxidelake-compute<br/>Gpu*Exec · CPU reference · conformance] --> device
storage[oxidelake-storage<br/>Parquet config · Arrow IPC spill · io_uring ObjectStore] --> memory
planner[oxidelake-planner<br/>HardwarePlacementRule · OxidePhysicalCodec] --> compute
tui[oxidelake-tui<br/>dashboard · oxidelake-tui-demo] --> core
runtime[oxidelake-runtime<br/>OxideSession · scheduler/worker wrappers · oxide CLI] --> planner
runtime --> storage
runtime --> tui
runtime --> ballista[Apache DataFusion Ballista 54<br/>scheduler · executors · Flight shuffle]
api[oxidelake-api<br/>DataFrame · SQL · optional PyO3] --> runtime
classDef ext fill:#e5e7eb,stroke:#9ca3af,color:#111827;
class ballista ext
flowchart TB
q[SQL / DataFrame] --> lp[DataFusion logical plan]
lp --> pp[Physical plan]
pp --> hpr[HardwarePlacementRule<br/>rewrite eligible nodes to Gpu*Exec]
hpr -->|embedded mode| local[In-process pipeline<br/>Arc<RecordBatch> over bounded mpsc]
hpr -->|cluster mode| sched[Ballista scheduler<br/>our session builder installs the rule<br/>our codec serializes Gpu*Exec]
sched --> e1[Ballista executor<br/>Gpu*Exec picks the local backend]
sched --> e2[Ballista executor<br/>Gpu*Exec picks the local backend]
e1 <-->|Arrow Flight shuffle - Ballista| e2
e1 --> result[Result stream]
e2 --> result
Placement is a physical optimizer rule, not an AST pass: eligibility depends on physical operator shape and resolved types (ADR-0010). Anything outside the bounded v1 GPU coverage stays on CPU. In cluster mode the rule runs on the scheduler against the declared cluster capability (OXIDE_CLUSTER_BACKEND); each Gpu*Exec then selects the real local backend at execute time, so a heterogeneous cluster stays correct (ADR-0013).
| Operator | GPU-eligible shape | Otherwise |
|---|---|---|
| filter + project | =, <, <=, >, >= combined with AND over Int64/Float64 vs literals; column selection; stream-compacted output |
CPU |
| hash join | inner join, single Int64 key; open-addressing table built with atomic CAS, probed in parallel | CPU |
| aggregate | SUM/COUNT/MIN/MAX over Int64/Float64, one low-cardinality Int64 key | CPU |
| vector distance | L2 and cosine, query vector vs FixedSizeList<Float32> column |
CPU |
CUDA implements all four; Metal implements filter + project and vector distance in v1.
flowchart LR
vram[Device VRAM<br/>128 B aligned] <-->|demote / promote| host[Pinned or pageable host RAM<br/>64 B aligned]
host <-->|Arrow IPC spill files via ObjectStore| disk[Local spill directory<br/>LocalFileSystem or UringLocalFileSystem]
- Every in-memory table is an Arrow
RecordBatch; host buffers are 64-byte aligned, device-bound buffers 128-byte aligned. - Pinned host memory is available: with
cudaactive and a driver present,GpuBackend::alloc_pinned_hostallocates page-locked memory (thecuMemHostAllocpath) for DMA without staging copies, falling back toAlignedBufotherwise. The path taken is observable asMemoryClass::{Pinned, Pageable}. - The operator layer does not use it in 0.x.
upload_bytescopies from a pageable ArrowBufferanddownload_columncopies into a pageableVec, so the operator path pays the staging copy that pinned memory exists to avoid;alloc_pinned_hostis exercised only by the T4 device tests. Pinned staging for operator transfers is audit item P6 (#26), tracked in the roadmap's Phase 8. Any transfer-throughput number measured today is a pageable number. - On Apple Silicon,
MTLResourceStorageModeSharedbuffers give CPU and GPU one physical allocation. SpillManagerholds per-tier budgets, demotes cold batches on high-watermark pressure, promotes on access, and exports metrics to the TUI. On a GPU-less machine the device tier is empty and the host ↔ disk path is fully testable.- It is a library, not part of the query path (#25). No operator registers a batch with it, so in a running query the tier gauges and the spill counters stay at zero however much memory the query used — a reading of zero there says nothing about the query. The dashboard states this rather than implying otherwise: the Telemetry panel's last line reads
spill library only: no query registers batches, andTelemetrySnapshot::capacity.spill_on_query_pathis the flag it renders. The gauges' capacities now come from the local backend'sMemoryInfo(the device's total for a GPU backend, host RAM for the CPU one) instead of the fixed constants they used to be drawn against, and a tier the engine has no number for is drawn without a ratio. - Wiring it in is not a matter of calling it from the join.
GpuHashJoinExec's build side is probed by every batch, so demoting it would trade an unbounded memory claim for an unbounded time one, and promoting it once puts it straight back in host RAM;GpuAggregateExeccollects its whole input before aggregating, so bounding it needs the streaming aggregate first. The honest version of this in 0.2 is the label; the engineering is Phase 8.
Arc<dyn GpuBackend> is selected at startup by HardwareDetector (CUDA → Metal → CPU, overridable with OXIDE_BACKEND). The trait is object-safe and synchronous; asynchrony lives on device streams (ADR-0005). CpuBackend is always present and fully functional — it is the semantic reference the GPU backends must match in the conformance suite.
Kernels ship as source: .cu compiled at runtime with NVRTC (PTX cached per device), .metal compiled with newLibraryWithSource. No nvcc, no Xcode step (ADR-0003, ADR-0004).
There is no private file format (ADR-0014).
flowchart LR
pq[Parquet lake data<br/>statistics · page index · Bloom filters] --> df[DataFusion Parquet scan<br/>prunes row groups and pages]
df --> os[object_store::ObjectStore<br/>LocalFileSystem | UringLocalFileSystem]
spill[SpillManager] <-->|Arrow IPC, 64 B aligned, no decode| os
os --> nvme[(Local NVMe)]
- Lake data is Parquet through DataFusion's listing tables;
oxidelake-storageowns the writer properties (row-group size, dictionaries, Bloom filters on keys, page statistics), the session pruning settings, and the tests that prove pruning happened via scan metrics while results stay identical. - Spill files and the optional hot cache are Arrow IPC: raw Arrow buffers, zero decode.
- The io_uring fast path is an
ObjectStoreimplementation behind theio-uringfeature; both stores pass one conformance test (ADR-0007).
TelemetryHub (in oxidelake-core) is the only coupling between engine and dashboard; in v1 it observes the local process only. The TUI is a state machine with a pure render(state, frame); repaints are bracketed in DEC 2026 synchronized updates. Four panels: Plan DAG with hardware tags, Inspector, Telemetry gauges, Describe (P25/P50/P99).
Three surfaces, in increasing cost to the reader (#33):
- One
INFOline per query, fromOxideSession::collect:query finished mode=embedded/cuda rows=98 batches=1 elapsed_ms=3.9 gpu_operators=2 fallback_batches=2. It is oncollectand not onsql, because aDataFramehas not run yet. Logs go to stderr, so they never mix intooxide sql --output json, and ANSI is off unless stderr is a terminal. - One span per
(operator, partition),oxide.operator, entered around each batch, with fieldsoperator,partition,target(what the plan says) andbackend(what is executing). The pair is the question these logs exist to answer; they differ exactly when the CPU-fallback counter is moving.GpuOperator's own methods run inside it and add no span of their own — a nested span there would carry no field the outer one does not. - A Prometheus endpoint,
oxide-worker --metrics-port/oxide-scheduler --metrics-port(featuremetrics):oxide_operator_*counters labelled by operator and backend, plus the tier gauges and spill counters. A cluster executor never runs the placement rule, so its operators are attached to a process-wideTelemetryHubby the plan codec; that hub is what is served. It is unauthenticated, like the Ballista ports themselves — bind it on a private interface. Without the feature the flag is refused rather than ignored.
OxideSession::telemetry() is the embedded hub and stays empty for a cluster session: the work happened in other processes, and each of those has its own endpoint.
The Inspector's cpu fallback line is the one counter that cannot be inferred from the others (#32). A GPU deployment that runs every batch on the CPU reference produces identical rows and identical EXPLAIN tags, so fallback_batches / batches is what tells the two apart; it is green at zero, yellow when some batches fell back and red when all of them did. The matching planner-side answer is the placement notes section oxide explain prints under the plan, naming each node the rule left on the CPU and why — a skipped node is an ordinary DataFusion operator in the plan above it, indistinguishable from one that was never eligible. Tested two ways: in-process with ratatui::backend::TestBackend + insta, and end-to-end with termlens driving the deterministic oxidelake-tui-demo binary in a real PTY (ADR-0011).
GPUDirect Storage / cuFile and GPU-side Parquet decode · tuning Ballista's retry or AQE behaviour · TLS/auth for the Ballista cluster · catalog/metastore (Iceberg, Delta) · transactions · cost-based optimization beyond DataFusion defaults · Lance integration (v2) · cluster-wide telemetry in the TUI · Windows.