Architecture

Architecture

View as Markdown

System Overview

Package Map

PackageResponsibilityKey types
environments/Base class, registry, @benchmark BYOB API, environment typesEvalEnvironment, SeedResult, VerifyResult, BenchmarkDefinition
benchmarks/17 built-in benchmarks (@benchmark + @scorer or @register)Scorer functions
solvers/Pluggable inference strategiesSolver, ChatSolver, VLMSolver, HarborSolver, GymSolver, ReActSolver, NatSolver, OpenClawSolver (config type: simple → ChatSolver/VLMSolver, harbor, tool_calling → ReActSolver, gym_delegation, etc.)
engine/Core eval loop, model client, checkpoint, comparison, export pluginsrun_evaluation(), ModelClient, CheckpointManager, InspectExporter, WandBExporter
config/Pydantic config schemas, env expansion, YAML compositionEvalConfig, parse_eval_config(), compose_config(), services.py, sandboxes.py, solvers.py, scoring.py, clusters.py
orchestration/Suite orchestration, model server management, SLURM generation, proxy lifecyclerun_local(), DeployConfig, generate_sbatch(), start_proxy()
serving/HTTP server for environments (evaluator + Gym protocol)generate_app()
executors/Executor protocol and backends (local, Docker, SLURM)Executor, get_executor(), detect_executor(), ProcessState
sandbox/Per-problem isolated execution and strategy patternsSandbox, SandboxSpec, SandboxManager, ECSFargateSandbox, NoSandbox, StatefulSandbox, StatelessSandbox
interceptors/LiteLLM proxy callback plugins for request/response interceptionresolve_interceptors(), BaseInterceptor
scoring/Verification scorers, judge pipeline, JSON schemaexact_match, code_sandbox, needs_judge
observability/Rich telemetry captureStepRecord, ModelResponse, RuntimeStats, ArtifactCollector
metrics/Statistical aggregationpass_at_k(), bootstrap_ci(), category_breakdown()
cli/CLI commandsnel eval run, nel eval report, nel eval merge, nel serve, nel validate, nel list, nel compare, nel gate, nel config, nel package

Environment Abstraction

Everything is an EvalEnvironment. Built-in benchmarks, remote Gym servers, NeMo Skills tasks, lm-eval harness tasks, and VLMEvalKit benchmarks all implement the same contract:

Resolution

The registry resolves environment names in order:

  1. URI schemelm-eval://task, skills://name, gym://host:port, gym://name, vlmevalkit://dataset, container://image#task
  2. Built-in registry — names registered via @benchmark or @register
1from nemo_evaluator import get_environment
2
3env = get_environment("mmlu") # built-in
4env = get_environment("lm-eval://aime25") # lm-eval task
5env = get_environment("skills://gpqa") # NeMo Skills
6env = get_environment("gym://localhost:9090") # remote Gym
7env = get_environment("vlmevalkit://MMBench_DEV_EN") # VLMEvalKit

Evaluation Flow

Solver Protocol

Solvers decouple inference strategy from benchmark logic. The eval loop calls solver.solve(task) and receives a response. In YAML configs, solvers are configured via solver.type in each benchmark.

SolverConfig typeProtocolUse case
ChatSolversimple/chat/completionsStandard benchmarks (default)
VLMSolversimple (images)/chat/completions + imagesVision-language benchmarks
HarborSolverharborHarbor agent SDKAgentic evaluation (OpenHands, SWE-agent)
ReActSolvertool_callingNEL-driven ReAct loopFull-observability tool use (Gym HTTP tools, sandbox tools, or both)
GymSolvergym_delegationHTTP to nemo-gymDelegate solve to gym server
NatSolver(via service)SSE /generate/fullNAT agent benchmarks
OpenClawSolveropenclawOpenClaw CLIOpenClaw benchmarks
1class Solver(Protocol):
2 async def solve(self, task: SeedResult) -> SolveResult: ...

Executor Protocol

All execution backends implement the Executor protocol (executors/__init__.py):

1class Executor(Protocol):
2 name: str
3 def run(self, config, *, dry_run=False, resume=False,
4 background=False, submit=False) -> None: ...
5 def status(self, output_dir) -> ProcessState: ...
6 def stop(self, output_dir) -> bool: ...
7 @staticmethod
8 def detect(output_dir) -> bool: ...

The CLI dispatches via get_executor(config.cluster.type) and detect_executor(output_dir) — no if/elif trees.

ExecutorConfigMetadata fileWhat it does
LocalExecutorcluster.type: localnel.pidIn-process eval with optional model deployment, checkpointing, and failure isolation
DockerExecutorcluster.type: dockerdocker.jsonRuns eval inside a Docker container with the correct per-harness image
SlurmExecutorcluster.type: slurmslurm_job.jsonGenerates self-contained sbatch scripts with per-benchmark containers

SLURM uses node_pools to declare resource topology. Services and sandboxes reference pools by name, enabling heterogeneous jobs (e.g., GPU nodes for model serving + CPU nodes for sandboxes).

Adding a new executor (e.g. Kubernetes) requires only a new class, a metadata file convention, and a registry entry.

$# Local with external API
$nel eval run --bench mmlu --model-url https://api.example.com/v1
$
$# Docker
$nel eval run config.yaml # with cluster.type: docker
$
$# SLURM (generates + submits sbatch)
$nel eval run config.yaml # with cluster.type: slurm

Model Deployment

The orchestration/model_server.py module manages model server lifecycle:

Config typeInternal classDescription
apiAPIDeploymentExternal API, no server management
vllmProcessModelDeploymentLocal vLLM process
sglangProcessModelDeploymentLocal SGLang process
trt_llmProcessModelDeploymentLocal TensorRT-LLM process

All deployments implement start() -> url, health_wait(), and stop().

Observability Data Model

Resilience and Resume

Multi-benchmark suites use CheckpointManager to track per-benchmark completion:

  • Failure isolation: A failing benchmark is caught, logged, and skipped. The suite continues to the next benchmark.
  • Checkpoint tracking: Each benchmark’s status (completed/failed) is persisted to disk under the output directory.
  • Resume: --resume skips completed benchmarks and retries failed ones. Without --resume, checkpoints are cleared for a fresh run.

Sharding

Sharding is transparent to the eval loop. problem_range changes the iteration bounds. The merge step recomputes global metrics from concatenated per-shard results.