OxideLake
A GPU-accelerated, Arrow-native distributed analytical query engine, written in Rust. It reads and writes Parquet on object storage; a catalog, a metastore and transactions are out of scope, so it is a query engine over a lake rather than a lakehouse in the full sense.
Status: internal phases 0–7 complete; published 0.2.0. Production readiness is milestone v0.2.0, and 0.2.0 did not finish it — ten Phase 8 lines remain open, three of them needing a CUDA device to verify on. What 0.2.0 does add is the ability to tell what a deployment is actually doing: a CPU-fallback counter, placement notes under oxide explain, query spans, a per-query log line and a Prometheus endpoint. The default build is pure CPU and the whole gate is green on Linux x86_64 and macOS arm64. Both GPU backends have executed on real hardware: Metal on Apple silicon (conformance-tested against stock DataFusion on an M4 Pro) and CUDA on an NVIDIA T4 since 2026-09-06, where every kernel was NVRTC-compiled and launched — and where a correctness bug in the aggregation kernel was found. STATUS.md holds the verification matrix, component by component, and is the ledger ADR-0012 makes it. The specification the engine was built to is docs/SPEC.md.
Install
Both give you the oxide, oxide-scheduler and oxide-worker binaries.
The Homebrew formula is generated by the release itself from the archives'
checksums, so it never describes a build that did not happen.
Or download a static archive for Linux (x86_64, aarch64, musl) or macOS
(Apple silicon, Intel) from the latest release,
verify it against the .sha256 beside it, and put the three binaries on your
PATH. Using the engine as a library is cargo add oxidelake-api.
What OxideLake is
OxideLake runs the same plans in two modes over the Apache Arrow columnar memory model:
- Embedded mode (DuckDB-like) — in-process DataFusion; operators exchange
Arc<RecordBatch>over bounded async channels with no serialization. - Cluster mode (Spark-like) — Apache DataFusion Ballista schedules stages across executors and streams shuffle partitions over Arrow Flight; OxideLake supplies the GPU operators, the placement rule and the plan codec that ships them to executors.
Acceleration backends: CPU (always available, the correctness reference), NVIDIA CUDA (opt-in feature, NVRTC-JIT at runtime), Apple Metal on unified memory (opt-in, macOS only, MSL compiled at runtime).
The differentiator is the operator layer: GpuFilterExec (fused filter + projection), GpuHashJoinExec, GpuAggregateExec and GpuVectorDistanceExec are DataFusion ExecutionPlans with their own CUDA and Metal kernels behind one object-safe hardware abstraction. A placement rule rewrites eligible plan nodes for the target backend — EXPLAIN shows GpuFilterExec[metal] — and every operator still selects the real local backend at execution time and falls back per batch to the CPU reference, which is what keeps a heterogeneous cluster correct. Storage is Parquet pruned by DataFusion's statistics, page index and Bloom filters, with Arrow IPC for zero-decode spill and caches.
Quickstart
Everything below runs as written (Rust 1.98 via rust-toolchain.toml; the workspace builds on 1.94.1 and up — or just make quickstart):
# 1M-row demo table: Parquet with Bloom filters on id and k, page statistics on
# embedded SQL over it
# vector search: l2_distance / cosine_distance are built-in SQL UDFs
# what the plan looks like for a GPU cluster: placement tags without needing the GPU
# the terminal dashboard over a real query: plan DAG, per-operator telemetry, column profiles
In-database inference (optional, --features predict)
Score a row against a trained model without leaving SQL. Off by default; it pulls in a tensor library.
# an oxmera Sequential/Linear model saved with safetensors, whose header
# declares {"__metadata__": {"oxidelake.activation": "relu"}}
# a checkpoint whose header says nothing: state the activation in the query
+----+--------------------------+
| id | logits |
+----+--------------------------+
| 0 | [-31.006792, 3.9050333] |
| 1 | [7.2003703, -4.237343] |
+----+--------------------------+
It composes with everything else — including the vector search that was already here, which is the point: OxideLake could find the nearest rows and could not score them.
The model file is read for its architecture, not just its weights: predict
rebuilds an oxmera::nn::Sequential of Linear layers from the 0.weight,
1.weight, … naming, and takes the activation between them from the
safetensors __metadata__ key oxidelake.activation — relu, gelu,
sigmoid, tanh or none, applied between layers and never after the last.
A file that declares nothing is refused, because the alternative is
scoring it with an activation it was not trained with and returning a
confident wrong number; the third argument predict(path, features, 'relu')
is how you say it yourself, and it must agree with the file when the file has
an opinion. The CPU-only execution, and why this is a UDF rather than an
operator, are recorded in
ADR-0015.
--target cpu|cuda|metal picks the placement target; execution always uses the hardware that is present (operators planned for an absent GPU take the per-batch CPU path — exactly what cluster executors do with a scheduler's plan).
Cluster mode
# OXIDE_CLUSTER_BACKEND declares the cluster's placement capability (default: cpu, no rewrites)
OXIDE_CLUSTER_BACKEND=cuda &
&
The end-to-end test in crates/oxidelake-runtime/tests/cli.rs spawns exactly this topology on ephemeral ports and asserts the cluster output is byte-identical to embedded mode.
DataFrame API
use *;
let session = local?;
session.register_parquet.await?;
let nearest = session
.table.await?
.filter?
.vector_distance?
.sort?
.limit?
.collect.await?;
The fluent verbs build ordinary DataFusion plans, so the placement rule lowers them the same way it lowers SQL — .vector_distance(…) plans GpuVectorDistanceExec[metal] on a Metal target.
GPU backends
# CUDA: compiles and lints with no CUDA installed (cudarc dynamic loading + NVRTC)
# Metal (macOS): the conformance suite executes on the device
# On a CUDA machine (run on a T4 since 2026-09-06; see STATUS.md):
OXIDE_BACKEND=cuda
Configuration
Every knob OxideLake reads, in one table. Flags win over environment variables; an explicit choice that the machine cannot honour is a startup error, never a silent fallback.
oxide
| Knob | Where | Default | What it does |
|---|---|---|---|
--query, -q |
sql, explain, tui |
— | The SQL statement (optional for tui, which then shows the demo model). |
--table, -t |
sql, explain, tui |
— | Registers NAME=PATH (a Parquet file or directory) before planning. Repeatable. |
--cluster |
sql |
— | Runs on a Ballista scheduler (df://host:port) instead of in-process. |
--target |
sql, explain, tui |
detected | Plans for cpu, cuda or metal. Placement only: execution uses the hardware that is present. Embedded mode only — on a cluster the scheduler decides. |
--batch-size |
sql, explain, tui |
8192, or 65536 on a GPU target | Rows per record batch. Batch boundaries never change results; larger batches amortise the host↔device round trip. 0 is refused. |
--output |
sql |
table |
table, json (an array of objects) or csv (RFC 4180 with a header). An empty result still prints the CSV header and [], so a script can tell "no rows" from "the query failed". |
--rows |
gen-data |
1000000 |
Rows in the demo table. |
--out |
gen-data |
— | Output directory; writes <out>/t.parquet. |
--seed |
gen-data |
42 |
PRNG seed. The same seed always writes the same bytes. |
--row-group-rows |
gen-data |
65536 |
Rows per Parquet row group. |
--compression |
gen-data |
zstd |
none, lz4 or zstd. |
oxide-scheduler
| Knob | Default | What it does |
|---|---|---|
--bind-host |
127.0.0.1 |
Address to bind. |
--port |
50050 |
gRPC port. |
--cluster-backend |
$OXIDE_CLUSTER_BACKEND, then cpu |
The capability the cluster declares, which is what placement rewrites against. |
--metrics-port / --metrics-host |
off / 127.0.0.1 |
As for the worker. A scheduler plans but does not execute, so its counters are empty; the endpoint exists so every process is scraped the same way. |
oxide-worker
| Knob | Default | What it does |
|---|---|---|
--scheduler-host / --scheduler-port |
localhost / 50050 |
The scheduler to join. |
--port |
50051 |
Arrow Flight port for shuffle data. |
--grpc-port |
50052 |
gRPC control port. |
--concurrent-tasks |
available parallelism | Tasks run at once. |
--work-dir |
a temporary directory | Where shuffle files go. |
--metrics-port / --metrics-host |
off / 127.0.0.1 |
Serve Prometheus metrics at /metrics (needs --features metrics). Unauthenticated: bind it on a private interface. Without the feature the flag is refused, never ignored. |
--backend |
$OXIDE_BACKEND, then detected |
The backend this worker executes on. Selected before the first task, so it reaches the operators; a backend the machine cannot provide is a startup error. |
Environment
| Variable | Read by | What it does |
|---|---|---|
OXIDE_BACKEND |
every process that executes operators | Forces cpu, cuda or metal instead of the detected backend. oxide-worker --backend overrides it. |
OXIDE_CLUSTER_BACKEND |
oxide-scheduler |
The cluster's declared placement capability (default cpu: no GPU rewrites). --cluster-backend overrides it. |
RUST_LOG |
every binary | tracing filter; logs go to stderr, so they never mix into --output json, and are uncoloured when stderr is not a terminal. RUST_LOG=oxidelake_runtime=info gives one line per query with the mode, rows, elapsed time and CPU fallbacks. |
Repository map
| Path | Purpose |
|---|---|
crates/oxidelake-core |
EngineError, BackendKind, operator parameter types, TelemetryHub |
crates/oxidelake-memory |
64/128-byte-aligned buffers, pinned/UMA allocators, 3-tier SpillManager (a library: no query path registers batches with it in 0.x — see architecture), Arrow IPC codec |
crates/oxidelake-device |
object-safe GpuBackend; CPU reference, CUDA (NVRTC) and Metal (MSL) backends; HardwareDetector |
crates/oxidelake-compute |
the four Gpu*Exec operators, per-batch CPU fallback, l2_distance/cosine_distance UDFs, conformance suite |
crates/oxidelake-storage |
Parquet writer/pruning config with proofs, Arrow IPC spill files, io_uring ObjectStore (Linux — implemented and conformance-tested, not yet used by sessions), gen-data generator |
crates/oxidelake-planner |
HardwarePlacementRule (+ vector-distance lowering before projection pushdown), OxidePhysicalCodec |
crates/oxidelake-runtime |
OxideSession (embedded + Ballista cluster), scheduler/worker wrappers, the oxide CLI, dashboard builder |
crates/oxidelake-tui |
ratatui dashboard, TestBackend snapshots and termlens PTY tests |
crates/oxidelake-api |
OxideFrame fluent DataFrame API and prelude |
crates/oxidelake-device/kernels/ |
CUDA C (.cu, NVRTC at runtime) and MSL (.metal, newLibraryWithSource) sources — inside the device crate so the published crate is self-contained (include_str! cannot reach outside a crate's directory at publish time; ADR-0016) |
docs/ |
roadmap · architecture · dependencies · verification · ADRs |
docs/SPEC.md |
The executable spec the build agent executed, phase by phase |
STATUS.md |
What is done, how it was verified, what was deferred — the honesty ledger |
Makefile · deny.toml |
make gate and friends; supply-chain policy for cargo deny |
Verification
Every change lands behind required-green and commit-policy. make gate
runs formatting, feature-specific Clippy, the test suites, all-feature rustdoc,
dependency-coherence and supply-chain checks, plus release and CI script tests.
CI separates the Linux feature tests and checks the
MSRV; macOS runs Metal and PTY tests and executes conformance when a Metal
device exists. Dependency caching reuses compilation while tests still run;
release CI retains a clean full gate. Known documentation-only changes may
skip Rust jobs under the tested verification policy.
The honesty rule applies throughout: nothing is claimed as working unless it
ran; GPU paths that only compiled are recorded as exactly that in
STATUS.md. The 2026-09-02 security and performance audit, with
every finding's status, is in docs/audit-2026-09-02.md.
Contributing
See CONTRIBUTING.md for the workflow (make help lists every target), SECURITY.md for the trust model and how to report vulnerabilities, and CHANGELOG.md for what changed.
License
Apache-2.0 — see LICENSE.