Text Generation
PEFT
Safetensors
English
pyspark
data-engineering
code-generation
qlora
lora
delta-lake
conversational
Instructions to use hoodarunner/pyspark-coding-assistant-lora with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use hoodarunner/pyspark-coding-assistant-lora with PEFT:
from peft import PeftModel from transformers import AutoModelForCausalLM base_model = AutoModelForCausalLM.from_pretrained("mistralai/Mistral-7B-Instruct-v0.3") model = PeftModel.from_pretrained(base_model, "hoodarunner/pyspark-coding-assistant-lora") - Notebooks
- Google Colab
- Kaggle
| """Execution harness: run candidate code against fixtures and judge the result. | |
| This is the part that makes spark-eval different from string-similarity scoring. | |
| Nothing here looks at the *text* of the generated code. It runs it and compares | |
| the DataFrame that comes out. | |
| Security note, stated plainly: `run_code` executes untrusted model output in | |
| this process. The import guard below stops casual accidents (a model that | |
| decides to `import os` and clean up after itself), not a determined adversary. | |
| If you are scoring untrusted checkpoints, run the CLI inside a container with no | |
| network and a read-only mount. | |
| """ | |
| from __future__ import annotations | |
| import builtins | |
| import math | |
| import signal | |
| from collections.abc import Iterator | |
| from contextlib import contextmanager | |
| from dataclasses import dataclass | |
| from typing import Any | |
| from pyspark.sql import DataFrame, SparkSession | |
| from .schema import Compare, Fixture, Task | |
| # Modules a correct PySpark answer never needs. Blocking them turns "the model | |
| # wandered off and deleted the fixtures" into a clean task failure. | |
| BLOCKED_IMPORTS = { | |
| "os", | |
| "sys", | |
| "subprocess", | |
| "shutil", | |
| "socket", | |
| "requests", | |
| "urllib", | |
| "urllib2", | |
| "urllib3", | |
| "httpx", | |
| "aiohttp", | |
| "pathlib", | |
| "ctypes", | |
| "importlib", | |
| "pickle", | |
| "multiprocessing", | |
| "tempfile", | |
| "glob", | |
| } | |
| class TaskTimeout(Exception): | |
| pass | |
| class BlockedImport(Exception): | |
| pass | |
| class ExecResult: | |
| """Outcome of running one piece of code against one task.""" | |
| ok: bool | |
| # "pass" | "error" | "timeout" | "no_solve" | "wrong_type" | |
| # | "schema_mismatch" | "row_mismatch" | "blocked_import" | |
| status: str | |
| detail: str = "" | |
| def __bool__(self) -> bool: # pragma: no cover - convenience only | |
| return self.ok | |
| def _time_limit(seconds: int) -> Iterator[None]: | |
| """Wall-clock cap on a block of code. | |
| SIGALRM only interrupts the driver thread, so a candidate that wedges deep | |
| inside a JVM call can outlive this. Per-task subprocess isolation is on the | |
| roadmap; until then, treat the timeout as best-effort. | |
| """ | |
| def _handler(signum, frame): # noqa: ANN001 | |
| raise TaskTimeout(f"exceeded {seconds}s") | |
| previous = signal.signal(signal.SIGALRM, _handler) | |
| signal.alarm(seconds) | |
| try: | |
| yield | |
| finally: | |
| signal.alarm(0) | |
| signal.signal(signal.SIGALRM, previous) | |
| def _guarded_import(name: str, *args, **kwargs): # noqa: ANN001, ANN202 | |
| root = name.split(".")[0] | |
| if root in BLOCKED_IMPORTS: | |
| raise BlockedImport(f"import of {root!r} is not allowed in a solution") | |
| return __import__(name, *args, **kwargs) | |
| def build_fixtures(spark: SparkSession, fixtures: list[Fixture]) -> dict[str, DataFrame]: | |
| """Materialise every fixture as a DataFrame keyed by its declared name.""" | |
| frames: dict[str, DataFrame] = {} | |
| for fx in fixtures: | |
| rows = [tuple(r) for r in fx.rows] | |
| frames[fx.name] = spark.createDataFrame(rows, schema=fx.schema) | |
| return frames | |
| def _extract_solve(code: str) -> Any: | |
| """exec `code` and hand back its `solve` callable. | |
| The builtins copy is shallow but private to this call, so swapping | |
| __import__ here cannot leak into the host process. | |
| """ | |
| safe_builtins = dict(vars(builtins)) | |
| safe_builtins["__import__"] = _guarded_import | |
| namespace: dict[str, Any] = {"__builtins__": safe_builtins, "__name__": "candidate"} | |
| exec(compile(code, "<candidate>", "exec"), namespace) # noqa: S102 | |
| solve = namespace.get("solve") | |
| if solve is None or not callable(solve): | |
| raise NameError("code does not define a callable named 'solve'") | |
| return solve | |
| # -------------------------------------------------------------------------- | |
| # Comparison | |
| # -------------------------------------------------------------------------- | |
| def _normalise(value: Any, tol: float) -> Any: | |
| """Make a collected value comparable and hashable. | |
| Floats are snapped to a tolerance grid so that two mathematically equal | |
| results computed in different partition orders land on the same key. | |
| Rows/structs, arrays and maps are flattened recursively. | |
| """ | |
| if value is None: | |
| return None | |
| if isinstance(value, bool): | |
| return value | |
| if isinstance(value, float): | |
| if math.isnan(value): | |
| return "__nan__" | |
| if math.isinf(value): | |
| return f"__inf_{'pos' if value > 0 else 'neg'}__" | |
| if tol > 0: | |
| return round(value / tol) * tol | |
| return value | |
| if isinstance(value, (list, tuple)): | |
| return tuple(_normalise(v, tol) for v in value) | |
| if isinstance(value, dict): | |
| return tuple(sorted((k, _normalise(v, tol)) for k, v in value.items())) | |
| if hasattr(value, "asDict"): # pyspark Row | |
| return tuple( | |
| sorted((k, _normalise(v, tol)) for k, v in value.asDict(recursive=True).items()) | |
| ) | |
| return value | |
| def _sort_key(row: tuple) -> tuple: | |
| """Total order over heterogeneous rows, including None. | |
| Sorting by the raw value blows up the moment a column mixes None with an | |
| int, which is exactly the case null-handling tasks are built around. Keying | |
| on (type name, repr) is stable and never raises. | |
| """ | |
| return tuple((v is None, type(v).__name__, repr(v)) for v in row) | |
| def _schema_signature(df: DataFrame, check_order: bool) -> Any: | |
| pairs = [(f.name, f.dataType.simpleString()) for f in df.schema.fields] | |
| return pairs if check_order else sorted(pairs) | |
| def compare_frames( | |
| expected: DataFrame, actual: DataFrame, cmp: Compare | |
| ) -> ExecResult: | |
| """Judge a candidate DataFrame against the reference DataFrame.""" | |
| if cmp.check_schema: | |
| exp_sig = _schema_signature(expected, cmp.check_column_order) | |
| act_sig = _schema_signature(actual, cmp.check_column_order) | |
| if exp_sig != act_sig: | |
| return ExecResult( | |
| False, | |
| "schema_mismatch", | |
| f"expected {exp_sig}, got {act_sig}", | |
| ) | |
| # Align column order before collecting so that a correct answer that simply | |
| # selected columns in a different order is not scored as wrong rows. | |
| if not cmp.check_column_order and set(expected.columns) == set(actual.columns): | |
| actual = actual.select(*expected.columns) | |
| tol = cmp.float_tolerance | |
| exp_rows = [tuple(_normalise(v, tol) for v in r) for r in expected.collect()] | |
| act_rows = [tuple(_normalise(v, tol) for v in r) for r in actual.collect()] | |
| if len(exp_rows) != len(act_rows): | |
| return ExecResult( | |
| False, | |
| "row_mismatch", | |
| f"expected {len(exp_rows)} rows, got {len(act_rows)}", | |
| ) | |
| if cmp.mode == "rows": | |
| exp_rows = sorted(exp_rows, key=_sort_key) | |
| act_rows = sorted(act_rows, key=_sort_key) | |
| for i, (e, a) in enumerate(zip(exp_rows, act_rows, strict=True)): | |
| if e != a: | |
| return ExecResult( | |
| False, | |
| "row_mismatch", | |
| f"first difference at row {i}: expected {e!r}, got {a!r}", | |
| ) | |
| return ExecResult(True, "pass") | |
| # -------------------------------------------------------------------------- | |
| # Entry points | |
| # -------------------------------------------------------------------------- | |
| def run_code( | |
| spark: SparkSession, task: Task, code: str, timeout: int = 60 | |
| ) -> tuple[ExecResult, DataFrame | None]: | |
| """Run one candidate against one task. Never raises on candidate errors.""" | |
| try: | |
| with _time_limit(timeout): | |
| solve = _extract_solve(code) | |
| frames = build_fixtures(spark, task.fixtures) | |
| result = solve(spark, **frames) | |
| if not isinstance(result, DataFrame): | |
| return ( | |
| ExecResult( | |
| False, | |
| "wrong_type", | |
| f"solve() returned {type(result).__name__}, expected DataFrame", | |
| ), | |
| None, | |
| ) | |
| # Force evaluation inside the time limit: Spark is lazy, so a | |
| # candidate that builds a broken plan would otherwise "pass" here | |
| # and explode later during comparison. | |
| result.cache() | |
| result.count() | |
| return ExecResult(True, "pass"), result | |
| except TaskTimeout as exc: | |
| return ExecResult(False, "timeout", str(exc)), None | |
| except BlockedImport as exc: | |
| return ExecResult(False, "blocked_import", str(exc)), None | |
| except NameError as exc: | |
| if "solve" in str(exc): | |
| return ExecResult(False, "no_solve", str(exc)), None | |
| return ExecResult(False, "error", f"{type(exc).__name__}: {exc}"), None | |
| except Exception as exc: # noqa: BLE001 - candidate code, anything goes | |
| detail = str(exc).strip().splitlines() | |
| head = detail[0] if detail else "" | |
| return ExecResult(False, "error", f"{type(exc).__name__}: {head[:400]}"), None | |
| def evaluate_candidate( | |
| spark: SparkSession, task: Task, code: str, timeout: int = 60 | |
| ) -> ExecResult: | |
| """Full pipeline for one candidate: run reference, run candidate, compare.""" | |
| ref_result, expected = run_code(spark, task, task.solution, timeout) | |
| if not ref_result.ok or expected is None: | |
| # This is a bug in the benchmark, not in the model. Surface it loudly. | |
| return ExecResult( | |
| False, | |
| "reference_broken", | |
| f"task {task.id}: reference solution failed: {ref_result.detail}", | |
| ) | |
| cand_result, actual = run_code(spark, task, code, timeout) | |
| if not cand_result.ok or actual is None: | |
| return cand_result | |
| return compare_frames(expected, actual, task.compare) | |