Skip to main content

GPU-accelerated DataFrame engine: Python ergonomics + Mojo AOT kernels, cross-vendor (NVIDIA/AMD/Apple).

Project description

๐Ÿš€ MXFrame

GPU-accelerated DataFrames โ€” Python ergonomics, Mojo speed, every GPU.

MXFrame is a DataFrame query engine that pairs a Polars-style Python API with pre-compiled Mojo AOT kernels. The same code runs on NVIDIA, AMD, and Apple Silicon โ€” no CUDA required, no JIT compilation at query time.

TPC-H Python License


โœจ Why MXFrame?

pandas Polars cuDF (Rapids) MXFrame
GPU support โŒ โŒ โœ… NVIDIA only โœ… Any GPU
Compiled kernels โŒ โœ… Rust โœ… CUDA โœ… Mojo AOT
Install complexity pip pip CUDA + Rapids stack pixi install
TPC-H competitive โŒ โœ… โœ… โœ…
Cross-vendor โŒ โŒ โŒ โœ… NVIDIA/AMD/Apple

MXFrame is the cuDF architecture without the CUDA lock-in.
The kernels are compiled once to a .so at build time โ€” loaded in ~1 ms at process start, then pure dispatch on every query. No per-query JIT tax.


โšก Quick Start

Install (Development)

# 1. Install pixi (Modular's package manager)
curl -fsSL https://pixi.sh/install.sh | bash

# 2. Clone and set up
git clone https://github.com/abhisheksreesaila/mxframe
cd mxframe
pixi install

# 3. Verify GPU is working
pixi run python3 scripts/_check_gpu.py

Your First Query

import pyarrow as pa
from mxframe import LazyFrame, Scan, col, lit

# Create an Arrow table (or load from Parquet/CSV)
data = pa.table({
    "dept":   pa.array(["eng", "eng", "mkt", "mkt", "eng"]),
    "salary": pa.array([120.0, 95.0, 80.0, 110.0, 130.0], pa.float32()),
    "age":    pa.array([32, 28, 35, 29, 40], pa.int32()),
})

# Build a lazy query plan โ€” nothing executes yet
result = (
    LazyFrame(Scan(data))
    .filter(col("age") > lit(28))
    .groupby("dept")
    .agg(
        col("salary").sum().alias("total_salary"),
        col("salary").mean().alias("avg_salary"),
        col("age").count().alias("headcount"),
    )
    .sort(col("total_salary"), descending=True)
    .compute(device="gpu")   # or "cpu"
)

print(result.to_pandas())

Output:

  dept  total_salary  avg_salary  headcount
0  eng         345.0       115.0          3
1  mkt         110.0       110.0          1

๐Ÿ“Š TPC-H Benchmark โ€” all 22 queries

Hardware: NVIDIA RTX 3090 (sm_86) ยท AMD 12-core CPU ยท Mojo 0.26.2 AOT kernels Baselines: Polars 1.29+ ยท Pandas 3.0 ยท MXFrame CPU path ยท MXFrame GPU path Data: TPC-H schema synthetic data (numpy RNG, fixed seed) Methodology: 1 warmup run + median of 3 timed runs, per engine. Warmup primes every cache an app would have primed on query #2 in production โ€” so these numbers reflect steady-state dispatch, not first-call JIT cost. Source of truth: scripts/bench_results_1M.csv and scripts/bench_results_10M.csv โ€” committed in repo, reproducible via scripts/benchmark_all_22.py

How the kernels dispatch

Device Path Coverage
CPU 100% ctypes into pre-compiled libmxkernels_aot.so All 22 queries โ€” group aggs, masked aggs, inner + left joins, gather, filter, sort, unique
GPU ctypes into pre-compiled libmxkernels_aot_gpu.so All grouped aggs (sum/min/max/count) + masked global aggs
GPU MAX Graph, shape-cached model Hash joins only โ€” compiled once per (n_left, n_right) shape, cached for the session

No per-query JIT on CPU. GPU aggregations skip MAX Graph entirely. GPU joins compile a model once per shape and reuse it โ€” the warmup run primes that cache.

1 M rows โ€” warm median of 3 runs

All times in milliseconds ยท lower is better. Speedup columns = Polars / MXFrame; bold = MXFrame wins.

Query Description MX CPU MX GPU Polars Pandas CPU vs Polars GPU vs Polars
Q1 Filter + 8 aggregations 11.2 94.4 35.6 117.4 3.2ร— 0.4ร—
Q2 Min-cost supplier 6.6 15.6 16.3 9.5 2.5ร— 1.0ร—
Q3 3-table join + agg 5.6 15.9 19.2 21.8 3.4ร— 1.2ร—
Q4 Order priority 15.0 192.5 15.0 29.9 1.0ร— 0.1ร—
Q5 Multi-join + groupby 0.6 4.1 22.6 22.0 37.7ร— 5.5ร—
Q6 Masked global agg 7.9 13.2 10.4 7.7 1.3ร— 0.8ร—
Q7 Shipping volume 0.6 7.3 29.9 19.1 49.8ร— 4.1ร—
Q8 Market share 0.9 4.7 20.2 10.3 22.4ร— 4.3ร—
Q9 Product profit (6-table join) 0.6 6.6 39.9 17.8 66.5ร— 6.0ร—
Q10 Customer revenue 3.6 11.0 32.6 19.3 9.1ร— 3.0ร—
Q11 Important stock 0.5 2.7 7.4 3.0 14.8ร— 2.7ร—
Q12 2-table join + agg 0.8 3.5 24.5 586.2 30.6ร— 7.0ร—
Q13 Customer distribution 16.2 30.2 27.5 33.5 1.7ร— 0.9ร—
Q14 Promo revenue 1.4 1.2 6.9 244.0 4.9ร— 5.8ร—
Q15 Top-supplier revenue 1.3 11.8 9.9 6.4 7.6ร— 0.8ร—
Q16 Part/supplier relationships 2.1 6.3 16.9 6.8 8.0ร— 2.7ร—
Q17 Small-qty order 0.3 2.7 7.9 4.7 26.3ร— 2.9ร—
Q18 Large-volume customers 4.2 22.3 33.4 16.8 8.0ร— 1.5ร—
Q19 Discounted revenue 10.9 11.2 19.6 22.4 1.8ร— 1.8ร—
Q20 Potential part promo 4.3 5.6 30.0 9.6 7.0ร— 5.4ร—
Q21 Suppliers who kept (EXISTS) 26.0 64.1 31.3 28.6 1.2ร— 0.5ร—
Q22 Global sales opportunity 7.6 16.5 25.4 56.7 3.3ร— 1.5ร—

1 M summary: MX CPU beats Polars on 21/22 queries (Q4 tied); MX GPU beats Polars on 16/22 queries. Headline CPU wins: Q9 66ร—, Q7 50ร—, Q5 38ร—, Q12 31ร—, Q17 26ร—. Headline GPU wins: Q12 7ร—, Q9 6ร—, Q14 5.8ร—, Q5 5.5ร—, Q20 5.4ร—.

10 M rows โ€” warm median of 3 runs

Query Description MX CPU MX GPU Polars Pandas CPU vs Polars GPU vs Polars
Q1 Filter + 8 aggregations 361.0 1190.7 946.5 1771.7 2.6ร— 0.8ร—
Q2 Min-cost supplier 5.7 11.9 15.4 7.6 2.7ร— 1.3ร—
Q3 3-table join + agg 57.7 67.2 72.2 581.8 1.3ร— 1.1ร—
Q4 Order priority 301.5 492.2 113.8 807.6 0.4ร— 0.2ร—
Q5 Multi-join + groupby 2.8 13.8 60.7 332.9 21.7ร— 4.4ร—
Q6 Masked global agg 399.6 523.2 92.0 246.4 0.2ร— 0.2ร—
Q7 Shipping volume 1.8 16.4 76.4 392.5 42.4ร— 4.7ร—
Q8 Market share 1.3 3.8 40.9 55.1 31.5ร— 10.8ร—
Q9 Product profit 0.7 7.4 89.7 431.3 128.1ร— 12.1ร—
Q10 Customer revenue 39.2 45.0 131.1 216.2 3.3ร— 2.9ร—
Q11 Important stock 0.4 3.1 6.6 2.5 16.5ร— 2.1ร—
Q12 2-table join + agg 1.3 4.4 116.4 6853.6 89.5ร— 26.5ร—
Q13 Customer distribution 385.1 396.1 285.8 463.9 0.7ร— 0.7ร—
Q14 Promo revenue 14.4 16.9 29.7 2719.2 2.1ร— 1.8ร—
Q15 Top-supplier revenue 2.7 57.4 16.1 30.0 6.0ร— 0.3ร—
Q16 Part/supplier relationships 2.7 6.5 16.3 6.8 6.0ร— 2.5ร—
Q17 Small-qty order 0.6 1.5 14.6 17.0 24.3ร— 9.7ร—
Q18 Large-volume customers 46.8 69.4 63.4 242.8 1.4ร— 0.9ร—
Q19 Discounted revenue 100.4 97.9 112.9 234.8 1.1ร— 1.2ร—
Q20 Potential part promo 32.3 37.1 39.9 52.9 1.2ร— 1.1ร—
Q21 Suppliers who kept 756.0 705.3 85.9 216.6 0.1ร— 0.1ร—
Q22 Global sales opportunity 54.1 59.2 132.8 1292.1 2.5ร— 2.2ร—

10 M summary: MX CPU beats Polars on 18/22 queries; MX GPU beats Polars on 15/22. Headline CPU wins scale beautifully: Q9 128ร—, Q12 89ร—, Q7 42ร—, Q8 32ร—, Q17 24ร—, Q5 22ร—. Headline GPU wins: Q12 26.5ร—, Q9 12ร—, Q8 10.8ร—, Q17 9.7ร—, Q7 4.7ร—.

Where MXFrame loses (same at both scales)

  • Q4, Q6, Q13, Q21 โ€” operations where our kernel path falls back to PyArrow compute or does extra passes. These are the focus of the next milestone (see roadmap.md).

What the numbers mean

  • Correctness โœ… โ€” all 22 queries return results that round-trip through Pandas and match Polars output.
  • Coverage โœ… โ€” every TPC-H query has a CPU AOT path; all group aggs/masked aggs have GPU AOT paths; GPU joins use shape-cached MAX Graph models.
  • No JIT tax in steady state โ€” after the first query of each shape warms the GPU join model cache, every subsequent call is pure dispatch. The CPU path has no JIT at all.
  • Why GPU doesn't always win โ€” GPU wins scale with workload size and kernel coverage. At 10 M, GPU crushes Polars on the join-heavy queries (Q8/Q9/Q12) where Mojo's shape-cached kernels pay off. Where GPU loses, it's either PCIe overhead on tiny outputs (Q1, Q6) or ops that still route through PyArrow fallback (Q4, Q13, Q21).

๐Ÿ” Reproducing the Benchmark

To run the benchmark with official TPC-H data (generated by DuckDB's faithful port of the TPC-H dbgen tool):

# Step 1 โ€” generate TPC-H data (requires: pip install duckdb)
#   SF=1  โ†’  ~6M lineitem rows,  ~200 MB Parquet
#   SF=0.1 โ†’ ~600K rows, quick sanity check
pixi run python3 scripts/gen_tpch_parquet.py --sf 1

# Step 2 โ€” run the 22-query benchmark against real data
pixi run python3 scripts/bench_real_tpch.py --data-dir data/tpch_sf1 --runs 3

The generated data/tpch_sf1/ directory contains 8 Parquet files (one per TPC-H table) that you can inspect, share, or version-control.

Scale factor guide

--sf lineitem rows approx size use case
0.01 ~60K 2 MB smoke test / CI
0.1 ~600K 20 MB local dev
1 ~6M 200 MB standard published benchmark
10 ~60M 2 GB stress test

Data lineage & legal note

  • Data is generated by DuckDB's TPC-H extension โ€” a faithful port of the official TPC-H dbgen v3.0.1 with the same value distributions (uniform, Zipfian, pseudo-random vocab).
  • TPC-Hยฎ is a trademark of the Transaction Processing Performance Council. These results are an independent, non-certified benchmark. They may not be reported as "TPC-H results" without formal TPC certification.
  • Reference: https://www.tpc.org/tpch/

๐Ÿ”ค API Reference

LazyFrame

from mxframe import LazyFrame, Scan, col, lit, when

lf = LazyFrame(Scan(arrow_table))
Method Description Example
.filter(expr) Row filter .filter(col("x") > lit(10))
.select(*cols) Column projection .select("a", "b", col("c").alias("d"))
.with_columns(*exprs) Add/replace columns .with_columns((col("a") * lit(2)).alias("a2"))
.groupby(*keys) Start grouped agg .groupby("dept", "region")
.join(other, left_on, right_on, how) Hash join .join(lf2, "id", "fk_id", how="inner")
.sort(expr, descending) Sort rows .sort(col("revenue"), descending=True)
.limit(n) Take first N rows .limit(100)
.distinct() Deduplicate rows .distinct()
.compute(device) Execute the plan .compute(device="gpu")

Expressions (col, lit, when)

# Arithmetic
col("price") * (lit(1.0) - col("discount"))

# Comparison
col("date") >= lit(19940101)

# Boolean combine
(col("x") > lit(0)) & (col("y") < lit(100))

# Conditional
when(col("nation") == lit("BRAZIL"), col("revenue"), lit(0.0))

# String
col("phone").startswith("13")

# Date parts
col("orderdate").year()    # extract year as int32

# Aggregations (inside .agg())
col("salary").sum()
col("salary").mean()
col("salary").min()
col("salary").max()
col("id").count()

SQL Frontend

from mxframe.sql_frontend import sql

result = sql("""
    SELECT dept, SUM(salary) AS total, COUNT(*) AS n
    FROM employees
    WHERE age > 30
    GROUP BY dept
    ORDER BY total DESC
""", employees=arrow_table)

๐Ÿ”ง Supported Operations

Category Operations
Filter >, >=, <, <=, ==, !=, &, |, ~, isin, startswith, contains
Aggregation sum, mean, min, max, count
Groupby Single key, multi-key, composite key
Join Inner, Left outer
Sort Single/multi column, ascending/descending
Window year() date part extraction
Projection select, with_columns, alias, arithmetic expressions
Semi-join Via unique-key inner join
Anti-join Via pc.is_in + pc.invert
Distinct Full row deduplication
SQL SELECT, FROM, WHERE, GROUP BY, ORDER BY, LIMIT, JOIN

๐Ÿ“ Project Structure

mxframe/
โ”œโ”€โ”€ __init__.py            โ† Public API (LazyFrame, Scan, col, lit, when, sql)
โ”œโ”€โ”€ lazy_frame.py          โ† LazyFrame, LazyGroupBy, Scan
โ”œโ”€โ”€ lazy_expr.py           โ† Expr, col(), lit(), when()
โ”œโ”€โ”€ compiler.py            โ† LogicalPlan โ†’ MAX Graph compiler
โ”œโ”€โ”€ custom_ops.py          โ† Dispatch: AOT kernels / MAX Graph / PyArrow fallback
โ”œโ”€โ”€ optimizer.py           โ† Plan rewrites (filter pushdown, join reordering)
โ”œโ”€โ”€ plan_validation.py     โ† Pre-execution plan checks
โ”œโ”€โ”€ sql_frontend.py        โ† SQL โ†’ LogicalPlan via sqlglot
โ”‚
โ”œโ”€โ”€ kernels_aot/           โ† Pre-compiled AOT shared libraries
โ”‚   โ”œโ”€โ”€ libmxkernels_aot.so      โ† CPU kernels (ctypes-callable)
โ”‚   โ””โ”€โ”€ libmxkernels_aot_gpu.so  โ† GPU kernels (CUDA/ROCm/Metal)
โ”‚
โ”œโ”€โ”€ kernels_v261/          โ† Mojo kernel source (build time only)
โ”‚   โ”œโ”€โ”€ group_sum.mojo, group_min.mojo, group_max.mojo ...
โ”‚   โ”œโ”€โ”€ join_scatter.mojo, join_count.mojo
โ”‚   โ”œโ”€โ”€ join_scatter_left.mojo, join_count_left.mojo
โ”‚   โ””โ”€โ”€ filter_gather.mojo, gather_rows.mojo, unique_mask.mojo ...
โ”‚
โ”œโ”€โ”€ kernels_aot/
โ”‚   โ”œโ”€โ”€ kernels_aot.mojo         โ† CPU AOT entry points
โ”‚   โ””โ”€โ”€ kernels_aot_gpu.mojo     โ† GPU AOT entry points
โ”‚
โ”œโ”€โ”€ scripts/
โ”‚   โ”œโ”€โ”€ bench_simple.py          โ† Clean 4-col benchmark (Pandas|Polars|MX CPU|MX GPU)
โ”‚   โ”œโ”€โ”€ benchmark_tpch.py        โ† All 22 TPC-H query implementations
โ”‚   โ”œโ”€โ”€ _test_aot_smoke.py       โ† AOT kernel smoke tests
โ”‚   โ””โ”€โ”€ quickstart.py            โ† Minimal hello-world example
โ”‚
โ””โ”€โ”€ docs/
    โ”œโ”€โ”€ vision-and-architecture.md
    โ”œโ”€โ”€ CONTRIBUTING.md          โ† Developer guide
    โ””โ”€โ”€ PUBLISHING.md            โ† pip release steps

๐Ÿ–ฅ๏ธ Device Selection

# CPU (default, works everywhere)
result = lf.compute(device="cpu")

# GPU (requires NVIDIA/AMD/Apple Silicon with MAX runtime)
result = lf.compute(device="gpu")

The GPU path uses Mojo's DeviceContext โ€” the same source compiles to:

  • PTX on NVIDIA (CUDA-compatible)
  • HSA/ROCm on AMD
  • Metal on Apple Silicon

๐Ÿงช Running Tests

# Smoke tests โ€” AOT kernels
pixi run python3 scripts/_test_aot_smoke.py

# All TPC-H correctness checks
pixi run python3 scripts/_test_phase6_tpch_tier2.py

# GPU check
pixi run python3 scripts/_check_gpu.py

# Full 22-query benchmark
pixi run python3 scripts/bench_simple.py --rows 1000000 --runs 3

๐Ÿ“ฆ Dependencies

Package Required Purpose
pyarrow >= 14 โœ… Column storage, zero-copy NumPy bridge
numpy >= 1.24 โœ… Vectorized pre/post processing
pandas >= 2.0 โœ… Reference implementations, Pandas bridge
modular >= 26.2 GPU only MAX Engine runtime, Mojo GPU dispatch
polars >= 0.20 optional Polars bridge + benchmark comparison
sqlglot >= 25 optional SQL frontend parsing

๐Ÿค Contributing

See CONTRIBUTING.md for the developer guide, kernel writing guidelines, and how to add new TPC-H queries.


๐Ÿ“„ License

Apache 2.0 โ€” see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mxframe-0.1.2.tar.gz (2.2 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mxframe-0.1.2-py3-none-manylinux_2_34_x86_64.whl (2.2 MB view details)

Uploaded Python 3manylinux: glibc 2.34+ x86-64

File details

Details for the file mxframe-0.1.2.tar.gz.

File metadata

  • Download URL: mxframe-0.1.2.tar.gz
  • Upload date:
  • Size: 2.2 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for mxframe-0.1.2.tar.gz
Algorithm Hash digest
SHA256 a2d78d404d235d12a4a0a0d77adbe70de9cf4738a3632ac3356868f463109290
MD5 671c92add3d61c682057818400af9f80
BLAKE2b-256 f516660c92291fe27e2baf9f851c7bdb8c8514fa77532c38360d8c5fbbf2e0ca

See more details on using hashes here.

Provenance

The following attestation bundles were made for mxframe-0.1.2.tar.gz:

Publisher: publish.yml on abhisheksreesaila/mxframe

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file mxframe-0.1.2-py3-none-manylinux_2_34_x86_64.whl.

File metadata

File hashes

Hashes for mxframe-0.1.2-py3-none-manylinux_2_34_x86_64.whl
Algorithm Hash digest
SHA256 705673d74302a58cbe9c817244b3c7f0698afeffc050fcf0102534352b1e1f96
MD5 b534b726e6ea01047765931b101fe68d
BLAKE2b-256 c27c119a5c1a6602aa012cee07f26dcd91ed1f07b37f62a7df370bc14f04e660

See more details on using hashes here.

Provenance

The following attestation bundles were made for mxframe-0.1.2-py3-none-manylinux_2_34_x86_64.whl:

Publisher: publish.yml on abhisheksreesaila/mxframe

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page