Running Evaluations with Marin¶
This guide shows the post-hoc evaluation API in Marin: how to evaluate an existing checkpoint by combining composable eval steps. For a high-level overview of the evaluation stack, see Evaluation Overview.
Prerequisites¶
- A trained model checkpoint. Evals take an
ArtifactStep[LevanterCheckpoint]handle — either the return value of a training run, or a pre-existing checkpoint wrapped withArtifactStep.adopt. - Access to the TPU or GPU resources the serving backend needs.
Each eval serves the model once as an OpenAI-compatible endpoint (marin-serve: vLLM or Levanter), evaluates the group's tasks against that URL with the evalchemy fork, and tears the server down. Multiple-choice and generation tasks run the same way (see Evaluation Overview).
Core APIs¶
The composable helpers live in experiments/evals/evals.py:
from experiments.evals.evalchemy.serve_and_eval import ServeSpec
from experiments.evals.evals import (
EvalGroup,
eval_step,
eval_steps,
eval_report,
core_evals,
key_evals,
base_model_evals,
)
- An
EvalGroupis one set of tasks evaluated against one served model. It is the composable unit: each group becomes one artifact, addressed byevaluation/evalchemy/{model}/{group_id}. eval_step(model, group)builds one eval artifact;eval_steps(model, groups)builds one per group.eval_report(results, name=...)aggregates a suite's results into oneEvalReportartifact.core_evals/key_evals/base_model_evalsare named menus — lists ofEvalGroupdrawn fromexperiments/evals/task_configs.py, the same task menu the in-loopEvalSuiteontrain_lmuses.
Each helper returns an ArtifactStep. Build them with run(...) (which returns the loaded artifacts)
or with StepRunner().run([lower(x) for x in steps]).
1. Run a named suite¶
from marin.execution.lazy import ArtifactStep, run
from marin.training.training import LevanterCheckpoint
from experiments.evals.evalchemy.serve_and_eval import ServeSpec
from experiments.evals.evals import eval_report, eval_steps, key_evals
# Adopt a pre-existing checkpoint as a typed handle (no copy, no recompute). A relative source
# resolves against the local bucket (MARIN_PREFIX, set by iris); pass an absolute gs:// path to pin.
model = ArtifactStep.adopt(
"perplexity-models/llama-200m",
"2026.06.30",
"gcsfuse_mount/perplexity-models/llama-200m",
kind=LevanterCheckpoint,
)
results = eval_steps(model, key_evals(serve=ServeSpec(tpu_type="v6e-8")))
report = eval_report(results, name=f"{model.name}/key")
if __name__ == "__main__":
run(report)
key_evals returns two groups: a generation group over KEY_GENERATION_TASKS and a multiple-choice
group over KEY_MULTIPLE_CHOICE_TASKS. eval_report depends on both and materializes the merged
per-task metrics.
core_evals and base_model_evals follow the same shape. base_model_evals runs CORE plus each MMLU
cut as its own group, so every cut is evaluated (each group has a distinct identity).
2. Compose your own groups¶
An EvalGroup states its tasks, its serving backend, and its id explicitly:
from marin.execution.lazy import run
from experiments.evals.evalchemy.serve_and_eval import ServeSpec
from experiments.evals.evals import EvalGroup, eval_report, eval_steps
from experiments.evals.task_configs import CORE_TASKS, KEY_GENERATION_TASKS
groups = [
EvalGroup(tasks=CORE_TASKS, id="core", serve=ServeSpec(tpu_type="v4-8")),
EvalGroup(tasks=KEY_GENERATION_TASKS, id="generation",
serve=ServeSpec(tpu_type="v6e-8"), max_gen_toks=4096),
]
report = eval_report(eval_steps(model, groups), name=f"{model.name}/custom")
if __name__ == "__main__":
run(report)
id is the task-group segment of the artifact name. Choose a stable id per group; two groups on one
model must have distinct ids so their outputs do not share an address.
3. Reading results back¶
resolve (or run) returns the typed artifacts. An EvalReport carries the merged metrics:
from marin.execution.lazy import resolve
report_artifact = resolve(report)
print(report_artifact.task_metrics) # {task: {metric: value}}
print(report_artifact.averages) # suite-level rollups
Each individual result is an EvalchemyResult; task_metrics() reads the per-task scores from the
evalchemy output tree.
4. Run the repository example scripts¶
The checked-in examples track real usage and are the safest starting points. They are
deferred-version CLIs (see marin.experiment.cli): --version supplies the run-wide version, the
plan prints without --run, and --run builds it.
# print the plan (no build)
uv run python -m experiments.evals.run_key_evals --version dev
# build it; --limit caps examples per task for a fast cluster smoke
uv run python -m experiments.evals.run_key_evals --version dev --run --limit 5
uv run python -m experiments.evals.run_base_model_evals --version 2026.07.16 --run
They adopt a checkpoint, build the suite, compile a report, and log results to W&B. --version dev
resolves the eval artifacts to a mutable version that rebuilds every run — pass a calendar version to
pin a run.
Serving on GPU¶
ServeSpec serves on TPU (tpu_type) or GPU (gpu_type, gpu_count). The GPU path runs the Marin
CUDA vLLM fork in an isolated uvx environment (IsolatedCudaVllm), which runs without nvcc — the
CoreWeave images ship CUDA libraries but no compiler. Set vllm_extra_args for models that would
otherwise JIT-compile a kernel at warmup.
Qwen gated-delta-net models (qwen_gdn_linear_attn: Qwen/Qwen3.5-35B-A3B, Qwen/Qwen3-Next-80B-A3B)
are the current case. Their default FlashInfer GDN prefill kernel is JIT-compiled, so without a compiler
the serve child dies at warmup (Could not find nvcc) and never registers its endpoint. Pass
--gdn-prefill-backend triton to use the compiler-free triton backend:
EvalGroup(
tasks=CORE_TASKS,
id="core",
serve=ServeSpec(
gpu_type="H100",
gpu_count=8,
tensor_parallel_size=8,
vllm_extra_args=("--gdn-prefill-backend", "triton"),
),
)
Other GPU-served models (DeepSeek-V2-Lite, Qwen3-30B-A3B, …) need no extra flags. See
ServeSpec.vllm_extra_args in serve_and_eval.py for the full list of recipes.
Parameter reference¶
EvalGroup¶
tasks: theEvalTaskConfigentries this group evaluates together against one served model.id: the group's identity segment in the artifact name.serve: aServeSpec— the serving backend (vllmorlevanter) and slice.tokenizer: HF tokenizer id the eval client loads; defaults to the served checkpoint. Set it to a base-model HF id when serving ags://path the eval image cannot load a tokenizer from.apply_chat_template: use the chat OpenAI route (local-chat-completions) instead of completions.max_gen_toks: generation length cap for generation tasks.max_eval_instances: optional cap on evaluated examples (a small value gives a fast smoke).num_concurrent: parallel in-flight requests the eval client sends the endpoint.discover_latest_checkpoint: whether to resolve the latest HF checkpoint under the model path.
eval_report¶
results: theEvalResultsteps to aggregate.name: the report's identity segment (evaluation/report/{name}).
For deeper dives, see:
- Evaluation Overview
experiments/evals/task_configs.pyexperiments/evals/evals.py