Benchmarking Small Language Models on natural language → Polars code generation
Hackathon date: 2026-04-18 · Team: Polaris (3 people)
Given a natural language question and a set of TPC-H table schemas, generate executable Python/Polars code that produces the correct DataFrame.
"Group the orders by customer and return the 10 with the highest total revenue."
→ result = (
orders
.group_by("o_custkey")
.agg(pl.col("o_totalprice").sum().alias("total_revenue"))
.sort("total_revenue", descending=True)
.head(10)
)
The submission is scored as:
Score = N / (T × VRAM^0.1 × RAM^0.01)
where N = correct outputs and T = total generation time. Correctness dominates everything — getting +30% matches on a model that uses 3× more VRAM is still a net ×2.4 gain.
Rather than submitting blind and waiting for remote results, we built a local pipeline that reproduces the official metric exactly:
dataset/gen_tpch.py — generates TPC-H tables locallydataset/executor.py — runs generated code in a restricted scope, captures result, handles exceptionsdataset/compare.py — column-order-invariant DataFrame comparison with diagnostic reason codesdata/seeds.jsonl — 25 validated seeds, each with question, schema, reference code, expected hash, tags, and difficultybenchmark_gemma.py — benchmarks Gemma 4 E2B with 4-tier scoring: generated → parses → runs → matches| Model | Params | VRAM | matches |
|---|---|---|---|
| LiquidAI LFM2-8B-A1B | 8B (1B active, MoE) | ~16 GB | 10% |
| Google Gemma 4 E2B-it | 5B (2.3B effective) | ~10 GB | 30% → 84% runs |
Gemma won by ×1.7 on the scoring formula despite being slower — correctness simply dominates.
| Run | Additions | runs | matches |
|---|---|---|---|
| Baseline | Minimal prompt | 40% | 30% |
| v2 | Polars cheat sheet + 4 few-shot + typed schema | 84% | 16%* |
*16% on a 2.5× harder seed set — not a regression. On shared seeds, v2 ≥ v0.
The cheat sheet explicitly forbids observed hallucinations: .with_column → .with_columns, pl.desc(x) → .sort(x, descending=True), df.len → df.height, .dense() → .rank(method="dense"), boolean indexing → .filter(...).
Ceiling hit: the remaining gap was not API errors but aliasing conventions — the model produces correct computations with different output column names (total_quantity vs the expected sum_qty). A prompt cannot fix a convention it has never seen.
A language model generates tokens one at a time with no hard guarantee about structure. Even with a detailed cheat sheet, the model can still:
.dense(), df.len)Prompt instructions are soft — a constraint grammar is hard.
dataset/polars_grammar.py builds a per-request Lark grammar that is instantiated with the actual table and column names for each question. This grammar covers:
pl.col(...), group_by, sort, over, joinfilter, select, with_columns, group_by, agg, join, sort, head, limit, unique, rename.alias, .sum/.mean/.min/.max, .rank(method=...), .over, .str.contains, .dt.year/.month/.day, .castThe grammar is fed to Outlines (with the llguidance backend) which compiles it into a logits processor. At each decoding step, the processor masks out any token whose addition would make the partial sequence unparseable by the grammar. The model can only emit structurally valid, schema-aware Polars code — hallucinations like pl.desc() or unknown columns are physically impossible.
# gemma_model.py
from outlines.types import CFG
grammar = build_grammar(tables) # per-request, injects real column names
response = outlines_model(prompt, CFG(grammar), max_new_tokens=512)
This is used in GemmaModel.generate_constrained() and wired into the three-level cascade in gemma_cascade.py:
L1 fast → greedy, no constraint (~2-3s)
→ if static validation fails → L2
L2 constrained → Outlines CFG on Polars grammar (~5-8s)
→ if still fails → L3
L3 retry → greedy with error feedback in the prompt
fallback → best of L1/L2
Static validation (looks_ok) checks AST validity, presence of result =, known anti-patterns, and whether all pl.col("x") references exist in the schema — without executing the code.
Polaris/
├── main.py # FastAPI server for submission
├── benchmark_gemma.py # Benchmark harness — Gemma 4 E2B
├── gemma_model.py # GemmaModel: greedy / constrained / retry
├── gemma_prompt.py # System prompt, few-shot examples, schema formatter
├── gemma_cascade.py # L1→L2→L3 cascade orchestrator + static validator
├── dataset/
│ ├── polars_grammar.py # Dynamic Lark CFG — schema-aware Polars grammar
│ ├── executor.py # Sandboxed code execution
│ ├── compare.py # Column-order-invariant DataFrame comparison
│ └── gen_tpch.py # TPC-H data generator
└── data/
├── seeds.jsonl # 25 validated TPC-H seeds
├── tpch/ # TPC-H parquet tables
└── expected/ # Reference output parquets
What worked
VRAM^0.1 is nearly flat, so quantization is a waste of time; correctness is everything.What didn't work as expected
llguidance dependency was absent on the submission VM — the CFG-constrained path, while implemented, couldn't be tested in time.The constrained decoding verdict
CFG grammar over generated Polars code eliminates structural and API hallucinations by construction, but adds 2-3× latency and requires the llguidance backend. The core insight: a prompt guides, a grammar enforces. For a benchmark with a hard correctness criterion, the guarantee matters.
Python
100.0%
Benchmarking Small Language Models on natural language → Polars code generation
Hackathon date: 2026-04-18 · Team: Polaris (3 people)
Given a natural language question and a set of TPC-H table schemas, generate executable Python/Polars code that produces the correct DataFrame.
"Group the orders by customer and return the 10 with the highest total revenue."
→ result = (
orders
.group_by("o_custkey")
.agg(pl.col("o_totalprice").sum().alias("total_revenue"))
.sort("total_revenue", descending=True)
.head(10)
)
The submission is scored as:
Score = N / (T × VRAM^0.1 × RAM^0.01)
where N = correct outputs and T = total generation time. Correctness dominates everything — getting +30% matches on a model that uses 3× more VRAM is still a net ×2.4 gain.
Rather than submitting blind and waiting for remote results, we built a local pipeline that reproduces the official metric exactly:
dataset/gen_tpch.py — generates TPC-H tables locallydataset/executor.py — runs generated code in a restricted scope, captures result, handles exceptionsdataset/compare.py — column-order-invariant DataFrame comparison with diagnostic reason codesdata/seeds.jsonl — 25 validated seeds, each with question, schema, reference code, expected hash, tags, and difficultybenchmark_gemma.py — benchmarks Gemma 4 E2B with 4-tier scoring: generated → parses → runs → matches| Model | Params | VRAM | matches |
|---|---|---|---|
| LiquidAI LFM2-8B-A1B | 8B (1B active, MoE) | ~16 GB | 10% |
| Google Gemma 4 E2B-it | 5B (2.3B effective) | ~10 GB | 30% → 84% runs |
Gemma won by ×1.7 on the scoring formula despite being slower — correctness simply dominates.
| Run | Additions | runs | matches |
|---|---|---|---|
| Baseline | Minimal prompt | 40% | 30% |
| v2 | Polars cheat sheet + 4 few-shot + typed schema | 84% | 16%* |
*16% on a 2.5× harder seed set — not a regression. On shared seeds, v2 ≥ v0.
The cheat sheet explicitly forbids observed hallucinations: .with_column → .with_columns, pl.desc(x) → .sort(x, descending=True), df.len → df.height, .dense() → .rank(method="dense"), boolean indexing → .filter(...).
Ceiling hit: the remaining gap was not API errors but aliasing conventions — the model produces correct computations with different output column names (total_quantity vs the expected sum_qty). A prompt cannot fix a convention it has never seen.
A language model generates tokens one at a time with no hard guarantee about structure. Even with a detailed cheat sheet, the model can still:
.dense(), df.len)Prompt instructions are soft — a constraint grammar is hard.
dataset/polars_grammar.py builds a per-request Lark grammar that is instantiated with the actual table and column names for each question. This grammar covers:
pl.col(...), group_by, sort, over, joinfilter, select, with_columns, group_by, agg, join, sort, head, limit, unique, rename.alias, .sum/.mean/.min/.max, .rank(method=...), .over, .str.contains, .dt.year/.month/.day, .castThe grammar is fed to Outlines (with the llguidance backend) which compiles it into a logits processor. At each decoding step, the processor masks out any token whose addition would make the partial sequence unparseable by the grammar. The model can only emit structurally valid, schema-aware Polars code — hallucinations like pl.desc() or unknown columns are physically impossible.
# gemma_model.py
from outlines.types import CFG
grammar = build_grammar(tables) # per-request, injects real column names
response = outlines_model(prompt, CFG(grammar), max_new_tokens=512)
This is used in GemmaModel.generate_constrained() and wired into the three-level cascade in gemma_cascade.py:
L1 fast → greedy, no constraint (~2-3s)
→ if static validation fails → L2
L2 constrained → Outlines CFG on Polars grammar (~5-8s)
→ if still fails → L3
L3 retry → greedy with error feedback in the prompt
fallback → best of L1/L2
Static validation (looks_ok) checks AST validity, presence of result =, known anti-patterns, and whether all pl.col("x") references exist in the schema — without executing the code.
Polaris/
├── main.py # FastAPI server for submission
├── benchmark_gemma.py # Benchmark harness — Gemma 4 E2B
├── gemma_model.py # GemmaModel: greedy / constrained / retry
├── gemma_prompt.py # System prompt, few-shot examples, schema formatter
├── gemma_cascade.py # L1→L2→L3 cascade orchestrator + static validator
├── dataset/
│ ├── polars_grammar.py # Dynamic Lark CFG — schema-aware Polars grammar
│ ├── executor.py # Sandboxed code execution
│ ├── compare.py # Column-order-invariant DataFrame comparison
│ └── gen_tpch.py # TPC-H data generator
└── data/
├── seeds.jsonl # 25 validated TPC-H seeds
├── tpch/ # TPC-H parquet tables
└── expected/ # Reference output parquets
What worked
VRAM^0.1 is nearly flat, so quantization is a waste of time; correctness is everything.What didn't work as expected
llguidance dependency was absent on the submission VM — the CFG-constrained path, while implemented, couldn't be tested in time.The constrained decoding verdict
CFG grammar over generated Polars code eliminates structural and API hallucinations by construction, but adds 2-3× latency and requires the llguidance backend. The core insight: a prompt guides, a grammar enforces. For a benchmark with a hard correctness criterion, the guarantee matters.
Python
100.0%