Data processing engines: Polars, DataFusion, Ray en Spark

Automatische vertaling Dit artikel is automatisch vertaald vanuit de oorspronkelijke Engelse versie.

Jarenlang verwerkte Pandas tabulaire data in memory en was Apache Spark verantwoordelijk voor de gedistribueerde variant. Die scheiding werkte prima zolang de data gestructureerd was.

Moderne pipelines verwerken ook afbeeldingen, audio en video. In deze workloads kan decoding op de CPU de GPU-inference uithongeren, terwijl garbage collection in de JVM en Python’s Global Interpreter Lock de throughput beperken. Nieuwe engines gebruiken Rust en Apache Arrow om die kosten te verlagen.

Om de opties te vergelijken, heb ik Pandas, Polars, DataFusion, Daft en native Rust gebenchmarkt op twee real-world datasets. Spark en Ray worden behandeld in afzonderlijke distributed notebooks. NYC taxi trips staan voor tabulair werk; Food-101-afbeeldingen staan voor een multimodale pipeline.

De engine-comparison-demo repository bevat de code. Draai deze op je eigen hardware, want de prestaties van een engine hangen af van de machine, de dataset en de workload.


Typen data-processing workloads

Engines zijn gespecialiseerd, dus de eerste vraag is welk type workload je daadwerkelijk hebt. Het grootste onderscheid is structured versus multimodal.

Twee werelden van data: structured versus multimodalTwee werelden van data: structured versus multimodal

Structured of tabulaire data omvat filtering, aggregatie en joins. Deze workloads zijn doorgaans CPU-bound en passen vaak in memory. Veel van zulke queries kunnen op één moderne machine draaien, maar memory, I/O, concurrency, recovery en de vorm van de workload bepalen nog steeds of een cluster passend is.

Multimodal AI verwerkt afbeeldingen, audio of video. Inference kan op een GPU draaien, terwijl decoding op de CPU, remote reads of batching de pipeline beperken. Welke stap de throughput begrenst, verandert met het instance-type en de operators. Meet daarom het gebruik over het volledige pad in plaats van de GPU geïsoleerd te dimensioneren.

De nieuwere engines richten zich op verschillende delen van deze ruimte. Native execution kan Python-overhead verlagen, Arrow-compatible interfaces kunnen data-uitwisseling goedkoper maken en streaming execution kan het memorygebruik begrenzen bij datasets die groter zijn dan RAM. Geen van deze voordelen is automatisch voor elke operator of conversie.


Deel 1: single-node processing

Pandas kiest voor een eager, in-memory programmeermodel. Veel gangbare operaties maken intermediates aan en parallel execution wordt niet via een query optimizer gecoördineerd. Die trade-off houdt exploratory code direct, maar kan duur worden bij grotere analytical scans. In Polars’ PDS-H benchmark met scale factor 10 had Pandas ongeveer 365 seconden nodig voor de suite, terwijl Polars’ streaming engine 3,89 seconden nodig had. Het resultaat van ongeveer 94x beschrijft die benchmark en configuratie; het is geen algemene conversiefactor van Pandas naar Polars.

Eager versus lazy executionEager versus lazy execution

Polars voor lokale tabulaire pipelines

Polars is een praktische default wanneer een lokale DataFrame-pipeline eager, single-process execution ontgroeit. De lazy API bouwt vóór execution een query plan op, waardoor optimalisaties zoals predicate pushdown en projection pruning mogelijk worden. De engine kan operators vervolgens parallel uitvoeren en, waar ondersteund, in streaming batches verwerken.

Het verschil wordt groter naarmate de data groeit. Bij scale factor 100 (~100 GB) voltooide Polars’ streaming engine de workload in 23,94 seconden, tegenover 152,27 seconden voor zijn eigen in-memory engine — ongeveer 6x sneller, op data die groter is dan RAM.

Het volgende is een illustratieve API-schets, geen letterlijk fragment uit de huidige engine_comparison_examples.ipynb of tabular.py:

import polars as pl

# scan_parquet reads only the schema — no data loaded yet
q = (
    pl.scan_parquet("yellow_tripdata_2024-01.parquet")
    .filter(
        (pl.col("trip_distance") > 5.0)
        & (pl.col("total_amount") > 30.0)
    )
    .group_by("payment_type")
    .agg(
        pl.col("total_amount").mean().alias("avg_fare"),
        pl.col("trip_distance").mean().alias("avg_distance"),
        pl.len().alias("trip_count"),
    )
)

# The entire plan is optimized and executed here, in parallel
result = q.collect()

Het belangrijke verschil zit in het execution model. In de bovenstaande lazy query kan Polars de filter en kolomselectie naar de Parquet-scan pushen. Daardoor kunnen row groups worden overgeslagen en ongebruikte kolommen buiten beschouwing blijven. Een typische eager Pandas-pipeline heeft geen query plan voor de volledige query dat kan worden geoptimaliseerd, hoewel zorgvuldig gebruik van Parquet-filters, geselecteerde kolommen en alternatieve backends een deel van het verschil kan herstellen.

DataFusion voor embedded query engines

Waar Polars een library is die je direct gebruikt, is DataFusion de engine waarop je andere engines bouwt. DataFusion vormt de basis voor InfluxDB 3.0, GreptimeDB en Apple’s Comet Spark accelerator.

In een ClickBench-run die het DataFusion-project in november 2024 publiceerde, behaalde DataFusion de beste resultaten van de geteste single-node Parquet-configuraties. Het Embucket-team publiceerde later een vendor case study voor TPC-H scale factor 1000 met datafusion-cli op r6gd.metal, met 64 vCPUs, 512 GB RAM, lokale NVMe en timings bij een hot cache. Q18 en Q21 faalden in de oorspronkelijke run of duurden buitensporig lang; voor de voltooide resultaten werden ze vervolgens structureel herschreven. Dit toont scale-up op één node onder die setup, niet een algemeen TPC-H-resultaat.

Het volgende is een illustratieve API-schets, geen letterlijk fragment uit de huidige engine_comparison_examples.ipynb of tabular.py:

from datafusion import SessionContext

ctx = SessionContext()
ctx.register_parquet("taxi", "yellow_tripdata_2024-01.parquet")

# SQL executed directly against Parquet — no intermediate copies
df = ctx.sql("""
    SELECT payment_type,
           COUNT(*)           AS trip_count,
           AVG(trip_distance) AS avg_distance,
           AVG(total_amount)  AS avg_fare
    FROM taxi
    WHERE trip_distance > 5.0 AND total_amount > 30.0
    GROUP BY payment_type
    ORDER BY trip_count DESC
""")
result = df.to_pandas()

De kracht van DataFusion is de modulariteit. De extension-API’s bieden ondersteuning voor custom catalogs, table providers, optimizer rules en execution plans. Daarom duikt DataFusion steeds op wanneer iemand een custom data platform bouwt of een query engine in een eigen product embedt.

Daft voor multimodale data

Daft maakt afbeeldingen, audio, video en embeddings onderdeel van de DataFrame-workflow. Het biedt native expressions voor operaties zoals image decoding en URL-downloads, waardoor gangbare preprocessing-paden geen row-by-row Python-loop nodig hebben. Die integratie is de belangrijkste reden om Daft boven een algemene tabulaire engine te overwegen.

Het volgende is een illustratieve API-schets, geen letterlijk fragment uit de huidige engine_comparison_examples.ipynb of multimodal.py:

import daft

df = daft.read_parquet("yellow_tripdata_2024-01.parquet")

result = (
    df.where(
        (daft.col("trip_distance") > 5.0)
        & (daft.col("total_amount") > 30.0)
    )
    .groupby("payment_type")
    .agg(
        daft.col("total_amount").mean().alias("avg_fare"),
        daft.col("trip_distance").mean().alias("avg_distance"),
        daft.col("trip_distance").count().alias("trip_count"),
    )
    .collect()
)

De API voelt vertrouwd aan als je Pandas kent, maar de engine gebruikt dezelfde Rust + Arrow-stack als Polars en DataFusion. Daft komt vooral tot zijn recht wanneer de “rows” in je DataFrame afbeeldingen, PDF’s of tensors zijn.

Single-node benchmark

Ik heb Pandas, Polars, DataFusion en Daft gedraaid, plus native Rust via Polars-rs, op ongeveer 41 miljoen NYC yellow taxi trips uit het volledige jaar 2024. Spark en Ray verschijnen in de distributed examples, niet in deze single-node benchmark. De volledige resultaten staan in de demo repo:

Benchmarkresultaten: single-node performance (gemengde timing scopes)Benchmarkresultaten: single-node performance (gemengde timing scopes)

De grafiek dient als context voor de bijbehorende benchmark, niet als een eerlijke apples-to-apples-ranking van engines: Pandas laadt de trip- en zonedata vóór zijn getimede functie, terwijl Polars, DataFusion, Daft en Rust verschillende file-read-, registratie- of query-setup-werkzaamheden binnen het getimede gedeelte uitvoeren. Het script herhaalt elke operatie drie keer en rapporteert de mediaan nadat imports zijn geladen. De toestand van de filesystem-cache, warm-up, CPU en andere runtimecondities kunnen de waarden veranderen. Breng de timinggrens op één lijn en voer de benchmark opnieuw uit onder gecontroleerde omstandigheden voordat je de volgorde als performance-resultaat interpreteert.

Bij deze omvang (~660 MB Parquet) zijn de drie nieuwere Python-engines snel klaar. Het getal van 94x voor Polars versus Pandas hierboven komt uit PDS-H; mijn workload met 41 miljoen rijen taxi-data leverde een kleiner verschil op. In deze run met gemengde timing scopes vielen Polars, DataFusion, Daft en Polars-rs binnen dezelfde brede bandbreedte, waarbij DataFusion de laagste gerapporteerde tijd noteerde. Die volgorde geldt voor deze query en deze meetgrens, niet voor de engines in het algemeen. Wanneer engines zo dicht bij elkaar liggen, moeten API-fit en herhaalde metingen op representatieve data de doorslag geven.


Deel 2: multimodale data

Tabulaire ETL verkleint data doorgaans: filteren, aggregeren en minder schrijven dan je leest. Multimodale AI-pipelines doen het tegenovergestelde. Eén documentpad kan uitwaaieren naar tientallen text chunks en embedding vectors.

In een conventionele PySpark-pipeline komen afbeeldingen en audio vaak binnen als binaire data en gaan ze naar Python-libraries voor decoding of transformatie. De grens tussen JVM en Python oversteken en resultaten serialiseren kan een groot deel van de wall-clock time innemen. Spark kan Arrow-based paths, vectorized UDFs en accelerator-integraties gebruiken, maar die keuzes vereisen expliciet design en metingen.

Pipelined execution en GPU-utilization

Spark plant werk in stages die door shuffle boundaries van elkaar zijn gescheiden. Een eenvoudige implementatie kan downloads, decoding en inference na elkaar uitvoeren, waardoor CPU- of GPU-capaciteit ongebruikt blijft. Spark ondersteunt GPU resource scheduling en plugins, dus idle time is niet onvermijdelijk. Overlap en backpressure zijn echter geen automatische eigenschappen van een gewone PySpark DataFrame-job.

Pipelined execution modelPipelined execution model

Pipelined execution is het alternatief. In plaats van stages na elkaar uit te voeren, laat de engine I/O, CPU-werk en GPU-inference overlappen. Wanneer stages en buffers goed in balans zijn, kan die overlap idle time verminderen en de throughput verbeteren; startup en drain, backpressure of een tragere stage kunnen er nog steeds voor zorgen dat resources wachten.

Image-processing benchmark

Ik heb image processing gebenchmarkt op 500 echte foto’s uit de Food-101-dataset. Resultaten uit de demo repo:

Benchmarkresultaten: multimodale performanceBenchmarkresultaten: multimodale performance

Polars en DataFusion ontbreken, omdat deze test zich richt op native image operations in plaats van tabulaire expressions. In deze run met 500 afbeeldingen was Daft’s image path ongeveer 3,7x sneller dan de Pandas + Pillow-baseline. De exacte waarde van multimodal_results.json, 1.0148517909983639s gedeeld door de waarde van rust_multimodal_results.json, 0.235865209s, is 4.30267692; de Rust-implementatie was dus 4,3x sneller. De bijbehorende README toont afgeronde waarden 1.01s en 0.24s, waarvan de ratio 4,2x is; dat komt door afronding in de weergave, terwijl de JSON-waarden leidend zijn. Het sample is nuttig om orchestration-overhead zichtbaar te maken, maar is op zichzelf te klein om production throughput te voorspellen.


Deel 3: distributed processing

Zodra één machine niet meer volstaat, moet je bepalen hoe je het werk verspreidt.

Apache Spark

Spark is een volwassen optie voor grote tabulaire ETL, shuffle-heavy joins en organisaties die het ecosysteem al beheren. Recovery op basis van lineage en brede platformondersteuning zijn belangrijk wanneer betrouwbaarheid en operationele vertrouwdheid zwaarder wegen dan lokale snelheid. Voor gemengde CPU/GPU-pipelines zijn bewustere resourceconfiguratie en pipeline-ontwerp nodig dan voor het SQL-pad. Daar bieden Ray Data en Daft een gerichtere abstractie.

Het volgende is een illustratieve Spark API-schets, geen letterlijk fragment uit de huidige distributed_spark.ipynb of spark_etl.py:

from pyspark.sql import SparkSession
from pyspark.sql import functions as F

spark = SparkSession.builder \
    .appName("TaxiETL") \
    .config("spark.sql.adaptive.enabled", "true") \
    .getOrCreate()

orders = spark.read.parquet("s3a://lake/taxi/*.parquet")
zones = spark.read.csv("s3a://lake/taxi_zones.csv", header=True)

# Spark excels at this: joining massive tables with a shuffle
result = (
    orders
    .filter(F.col("trip_distance") > 5.0)
    .join(zones, orders.PULocationID == zones.LocationID, "inner")
    .groupBy("Borough")
    .agg(
        F.sum("total_amount").alias("total_revenue"),
        F.avg("total_amount").alias("avg_fare"),
    )
    .orderBy(F.desc("total_revenue"))
)

Ray Data voor heterogene compute

Ray Data is ontworpen rond AI-workloads. In plaats van de stage barriers van Spark gebruikt Ray Data een streaming model dat GPU’s gevoed houdt. De belangrijkste feature voor AI-pipelines is mixed resource scheduling: je kunt declareren dat één actor “1 GPU, 4 CPUs” wil en een andere alleen CPU’s nodig heeft, waarna Ray de rest afhandelt.

Amazon rapporteerde meer dan $120 miljoen aan jaarlijkse besparingen na het migreren van geselecteerde interne data-processing workloads van Spark naar Ray. Het rapport noemt 91% betere cost efficiency in de proof of concept en 82% in productie per GiB S3-input. De schaal is opvallend, maar dit is een migration case study voor specifieke workloads en geen verwachte besparing bij een doorsnee Ray-adoptie.

Het volgende is een illustratieve Ray Data API-schets, geen letterlijk fragment uit de huidige distributed_ray.ipynb of ray_inference.py:

import ray

ray.init()

ds = ray.data.read_images("s3://my-bucket/food101/")

class ImageClassifier:
    def __init__(self):
        import torch
        from torchvision.models import resnet18, ResNet18_Weights
        self.model = resnet18(weights=ResNet18_Weights.DEFAULT).cuda()
        self.model.eval()
        self.preprocess = ResNet18_Weights.DEFAULT.transforms()

    def __call__(self, batch):
        import torch
        tensors = torch.stack([
            self.preprocess(img) for img in batch["image"]
        ]).cuda()
        with torch.no_grad():
            preds = self.model(tensors)
        return {
            "prediction": preds.argmax(dim=1).cpu().numpy(),
            "confidence": preds.max(dim=1).values.cpu().numpy(),
        }

# ActorPoolStrategy creates persistent GPU workers
predictions = ds.map_batches(
    ImageClassifier,
    compute=ray.data.ActorPoolStrategy(size=4),
    num_gpus=1,
    batch_size=64,
)
predictions.write_parquet("s3://output/predictions/")

De verhouding tussen CPU’s en GPU’s verdient hier aandacht. In Anyscale’s benchmarks bleef Ray Data schalen naarmate het aantal CPU’s per GPU toenam, terwijl de andere geteste engines een plateau bereikten. De bron rapporteert tot 3x speedup bij image inference wanneer CPU starvation afneemt, omdat de CPU-feeders de GPU kunnen blijven bijhouden.

Distributed Daft

Daft schaalt uit via zijn Flotilla engine: één Swordfish worker per node, met Flotilla erbovenop voor cluster-wide scheduling. Swordfish verzorgt lokale Rust execution en pipelinet I/O met compute via small-batch streaming, zodat elke node actief blijft zonder op de volgende stage te wachten.

In benchmarks die Daft publiceerde, was Flotilla 4–18x sneller dan de geteste Spark-implementaties over vier multimodale workloads. Het grootste verschil zat in een workload voor video-object-detection. Beschouw dit als bewijs dat het execution design belangrijk is voor deze pipelines; vergelijk het vervolgens met de concurrerende Anyscale-resultaten hieronder en met een lokale test.

Anyscale, het bedrijf achter Ray, publiceerde een concurrerende benchmark waarin Ray Data het verschil op high-CPU-instances na tuning verkleinde of omkeerde. Samen vormen de twee vendor studies een reden om representatieve operators en CPU/GPU-ratio’s te testen, niet om er een blijvende league table uit af te leiden.

Het volgende is een illustratieve Daft API-schets, geen letterlijk fragment uit de huidige distributed_daft.ipynb of daft_pipeline.py:

import daft
import numpy as np
from daft import col

# Daft's Flotilla engine distributes work across the cluster
df = daft.read_parquet("s3://data-lake/pdf_metadata/*.parquet")

# Download PDFs — Daft parallelizes downloads in Rust
df = df.with_column("pdf_bytes", col("pdf_url").download())

# Define a GPU class UDF for batched embedding generation
@daft.cls(gpus=1)
class TextEmbedder:
    def __init__(self):
        from sentence_transformers import SentenceTransformer
        self.model = SentenceTransformer("all-MiniLM-L6-v2", device="cuda")

    @daft.method.batch(
        return_dtype=daft.DataType.fixed_size_list(daft.DataType.float32(), 384)
    )
    def __call__(self, text_col):
        texts = text_col.to_pylist()
        embeddings = self.model.encode(texts, batch_size=32)
        return np.asarray(embeddings, dtype=np.float32)

# Daft schedules CPU downloads and GPU embeddings simultaneously
embedder = TextEmbedder()
df = df.with_column("embedding", embedder(col("text")))
df.write_parquet("s3://output/embeddings/")

Distributed comparison

FeatureSparkRay DataDaft (Flotilla)
Execution modelTask-per-core, partition-basedStreaming tasks en actorsSwordfish-per-node, streaming batches
StrengthsMassive SQL/ETL, fault toleranceHeterogeneous compute, GPU saturationMultimodal pipelining, bounded memory
GPU controlsResource scheduling plus ecosystem pluginsExpliciete task- en actor-resourcesIntegrated CPU/GPU pipeline scheduling
Typical tuningExecutors, memory, partitions, acceleratorsBatches, actors, object storeBatches, resources, I/O concurrency
Good evaluation caseGrote SQL/ETL- en shuffle-heavy jobsTraining of inference met gemengde resourcesMultimodale ingestion en transformatie

Deel 4: de rol van Rust en Arrow

Onder de concurrentie is de convergentie het interessantere verhaal. Polars, Daft en DataFusion gebruiken Rust uitgebreid, terwijl Ray naast zijn Python-API’s native componenten bevat. Ze kunnen allemaal data uitwisselen via onderdelen van het Apache Arrow-ecosysteem.

Convergentie van Rust en ArrowConvergentie van Rust en Arrow

De Arrow PyCapsule Interface (__arrow_c_stream__ protocol) geeft compatible libraries een standaardmethode om Arrow-streams uit te wisselen. Transfers kunnen row-wise serialization vermijden en buffers mogelijk hergebruiken wanneer schema’s en memory layouts compatibel zijn. Materialization, rechunking, typeconversie of device transfer kunnen nog steeds data kopiëren. Controleer de handoff daarom met profiling in plaats van aan te nemen dat deze gratis is.

Het volgende is een illustratieve API-schets, geen standalone script: er wordt van uitgegaan dat events.parquet al bestaat. De bijbehorende engine_comparison_examples.ipynb maakt .data/events.parquet aan in de setup-cellen vóór het Arrow-voorbeeld.

from datafusion import SessionContext
import polars as pl

# Compute in DataFusion (Rust-native execution)
ctx = SessionContext()
ctx.register_parquet("events", "events.parquet")
df = ctx.sql("""
    SELECT user_id, COUNT(*) as event_count
    FROM events WHERE event_type = 'purchase'
    GROUP BY user_id HAVING COUNT(*) > 5
""")

# Convert through Arrow; compatible buffers may be reused
arrow_table = df.to_arrow_table()
df_polars = pl.from_arrow(arrow_table)

# Continue analysis in Polars
result = df_polars.with_columns(
    pl.col("event_count").rank().alias("rank")
).sort("rank")

Waarom gebruiken deze nieuwere engines Rust voor native execution?

  1. Geen tracing garbage collector in Rust-code. Ownership geeft native operators directere controle over allocation en deallocation. Dat kan GC-gerelateerde latency verlagen, maar voorkomt geen memory pressure of out-of-memory failures.

  2. Compacte native representaties. Rust-structuren bevatten geen Java object headers. Het praktische voordeel hangt af van de layout van de engine: columnar JVM-systemen vermijden ook dat elke waarde als afzonderlijk object wordt gerepresenteerd.

  3. Sterkere concurrency checks. Safe Rust sluit veel data races al tijdens compile time uit. Engine-code kan nog steeds unsafe blocks en logische concurrency bugs bevatten, maar de taal verkleint het failure surface.

In combinatie met Arrow’s columnar memory layout kunnen deze keuzes allocation- en serialization-overhead verlagen. Ze verwijderen die overhead niet bij elke boundary; Python objects, network transport, incompatibele schema’s en device movement blijven relevant.


Deel 5: meerdere engines adopteren

Je hoeft niet het hele platform door één engine te laten lopen. Composition is nuttig wanneer de handoff tussen engines minder kost dan één systeem elke workload laten afhandelen.

Het coexistence-model: multi-engine-pipelineHet coexistence-model: multi-engine-pipeline

Een mogelijke relay gebruikt Spark voor gevestigde lakehouse-joins, schrijft een open table- of file format en geeft het resultaat door aan Ray Data of Daft voor GPU-inference of multimodale transformaties. Polars of DuckDB kunnen lokale analyse op dezelfde files verzorgen. Een kleiner team heeft misschien maar één van deze engines nodig; voeg een andere toe wanneer een gemeten bottleneck de operationele grens rechtvaardigt.

Deze composition is mogelijk dankzij interoperability. Parquet is in deze voorbeelden de gemeenschappelijke file handoff, terwijl Arrow in-memory interchange kan bieden. Ondersteuning voor Delta en Iceberg hangt af van de engine en connector, dus controleer elke boundary. Ook een file handoff brengt I/O- en conversiekosten met zich mee; die is niet automatisch gratis.

Engine-selectie per evaluatiecase

Evaluation caseShortlistValidate
Lokale analytische DataFramesPolars / DuckDBAPI-fit, memory, querymix
Bestaande distributed SQL/ETLApache SparkShufflegedrag, operations, kosten
Python-native parallel werkDask / RayScheduler-overhead, failure model
Gemengde CPU/GPU-training of -inferenceRay Data / DaftAccelerator-utilization, backpressure
Multimodale ingestionDaft / Ray DataNative operators, retry behavior
Embedded query engineDataFusionExtension-API’s, SQL coverage

Diagonal scaling

Lange tijd was de keuze: scale up (een grotere machine) of scale out (meer machines). Polars Cloud doet iets daartussenin, wat zij diagonal scaling noemen. Scale horizontaal terwijl je uit cloud storage leest om I/O maximaal te benutten, en schakel daarna over naar één grote node zodra filters en aggregaties de data hebben verkleind. De distributed shuffle vindt nooit plaats.

De bredere les is dat je de kosten per succesvolle job moet vergelijken, niet de hourly instance price. Een grotere node kan goedkoper zijn wanneer de runtime-reductie opweegt tegen het hogere tarief, maar het resultaat hangt af van I/O, memory pressure en de hoeveelheid werk die vóór een shuffle wordt geëlimineerd.


De vergelijking reproduceren

De bijbehorende demo repository bevat de scripts, notebooks en environment die voor de vergelijkingen zijn gebruikt. Het onderstaande quick command gebruikt de standaard full-year 2024 taxi-dataset van de repository, overeenkomstig met de grotere run die eerder is getoond.

# Install dependencies
uv sync

# Run the tabular benchmark (~41M NYC taxi trips from full-year 2024)
uv run python -m engine_comparison.benchmarks.tabular

# Run the multimodal benchmark (500 real food photos)
uv run python -m engine_comparison.benchmarks.multimodal

# Run native Rust benchmarks (Polars-rs + image crate)
cd rust_benchmark && cargo run --release && cd ..

De repo bevat ook notebooks voor side-by-side API-vergelijkingen en Docker Compose-setups voor lokale distributed runs van Spark, Ray en Daft.


Belangrijkste conclusies

  1. Bewijs eerst dat één machine niet volstaat. Scale-up-resultaten laten zien dat sommige grote analytical scans op één node passen. Meet memory, I/O, spill en recovery requirements voordat je cluster-overhead accepteert.

  2. Execution models zijn belangrijker dan vertrouwde syntax. Lazy planning, pushdown, streaming en parallel operators verklaren een groot deel van het verschil tussen een eager DataFrame-script en een analytical engine.

  3. Meet de volledige accelerator-pipeline. Decoding, network I/O, batching, serialization en backpressure bepalen of een GPU actief blijft. Ray Data en Daft maken deze overlap tot een centrale abstractie; Spark kan dit ondersteunen met extra design en tooling.

  4. Maak een shortlist op basis van de workload en benchmark daarna. Gebruik ecosystem fit om het veld te verkleinen en een representatieve end-to-end job om te kiezen. Vendor benchmarkresultaten zijn hypotheses, geen garanties.

  5. Open formats houden de keuze omkeerbaar. Arrow-compatible interfaces, Parquet en open table formats kunnen handoff-kosten verlagen. Controleer of een specifieke conversie buffers hergebruikt of kopieert.

Als je een startpunt nodig hebt, probeer dan Polars voor een lokale tabulaire pipeline en Daft of Ray Data voor een gemengde CPU/GPU-pipeline. Kies Spark wanneer de distributed execution, het ecosysteem of de bestaande operationele basis een requirement oplost. Datasetgrootte op zichzelf is een zwakke reden.


Referenties

Benchmarks en performancegegevens

Engines en frameworks

Architectuur en ecosysteem

Demo repository

  • engine-comparison-demo - Bijbehorende code voor dit artikel: benchmarks, notebooks en Docker Compose voor Spark/Ray/Daft

Datasets