Skip to content

Benchmark MCP query/tile tasks on the DuckDB + httpfs backend (methodology + standing results) #250

Description

@cboettig

Tracking issue for benchmarking the MCP query/tile tasks on the DuckDB + httpfs backend — establishing where performance stands, what's rate-limiting, and pushing further. Consolidates the bench scripts on branch bench/s3-ceiling (s3-throughput / s3-ceiling / s3-diag / s3-decomp + the engine/mean-carbon probes).

TL;DR (so far)

  • Real DuckDB queries are decode/compute-bound, not network- or httpfs-bound. S3 ≈ local ≈ internal-MinIO for heavy aggregates; throughput scales ~linearly with threads.
  • DuckDB local/httpfs parquet reads are GB/s-class (uncompressed) and competitive with Polars / faster than PyArrow. Earlier alarming "~180–250 MB/s" numbers were measurement artifacts (see invalid probes).
  • Column pruning is the dominant lever (~10×): 1-col sum ~16 GB/s vs 7-col ~1.6 GB/s uncompressed (local NVMe).

Methodology — valid metrics

  • Report uncompressed GB/s and Mrow/s, never compressed-byte MB/s. Carbon hex compresses ~4–6×, so compressed-byte rates understate decode by that factor.
  • Use queries that actually read+process data: sum()/avg() over real columns, or full materialize.
  • Determine rate-limiting two ways: thread scaling (scales → compute-bound; flat → I/O-bound) and local-vs-remote (equal → compute-bound).
  • Set DuckDB memory_limit explicitly: in a container DuckDB sizes it from host RAM and gets cgroup-OOMKilled at the pod limit.

Invalid probes — DO NOT use these to measure read throughput

  • count(*) — answered from parquet row-count metadata; reads ≈ no data.
  • count(COLUMNS(*)) — answered from per-column null-count metadata; reads ≈ no data. (This invalidated an earlier "DuckDB ~180 MB/s ceiling" claim — it was measuring footers, not transfer.)
  • read_blob as a proxy for query throughput — measures sequential whole-file transfer that does not parallelize across files (~95 MB/s over httpfs). read_parquet parallelizes ranged reads and is 13–23× faster over the same httpfs. Use read_blob only to characterize raw transport, never as the query-path number.
  • Environmental confounds to control: client NIC (cirrus has a 1 Gb/s card; the MinIO external-IP hairpin capped reads at ~1 Gb/s — fixed by using the internal minio-svc.minio.svc.cluster.local:9000), client CPU on small pods, and warm vs cold caches.

Standing results

Engine comparison, local NVMe (ZFS, warmed), carbon hex, uncompressed GB/s:

query DuckDB Polars PyArrow
sum 1 col 16.6 9.5 5.4
read all 7 cols 1.6 1.6 0.38

Representative MCP aggregates — cirrus pod (128 core), httpfs → internal MinIO, full carbon hex (4.84B rows / 13.7 GB):

query T=1 T=8 T=16 bound by
mean carbon / h0 (122 grp) 100.3s 10.6s 6.2s CPU (9.4× then 1.7× scaling)
mean carbon / h1 (h3 rollup) 25.1s 14.1s CPU (extra col + h3 on 4.84B rows)

Next steps

Refs: #249 (throughput bench), #103 (httpfs no conn-pool / DNS ceiling), #237 (httpfs large-chunk bug, fixed in 1.5.4), #42 / #227 (GPU path).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions