> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs.nvidia.com/nemo/evaluator/nightly/architecture/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.nvidia.com/nemo/evaluator/_mcp/server. # Architecture ## System Overview ```mermaid flowchart TB subgraph CLI["CLI (nel)"] RUN["nel eval run"] SERVE["nel serve"] VALIDATE["nel validate"] REPORT["nel eval report"] COMPARE["nel compare"] GATE["nel gate"] end subgraph ENVS["Environments"] REG["Registry"] BYOB["@benchmark + @scorer"] GYM["GymEnvironment"] SKILLS["SkillsEnvironment"] LMEVAL["LMEvalEnvironment"] VLM["VLMEvalKitEnvironment"] end subgraph ENGINE["Engine"] LOOP["eval_loop"] SOLVER["Solver"] MC["ModelClient"] end subgraph CFG["Config"] EVALCFG["EvalConfig"] COMPOSE["compose"] end subgraph ORCH["Orchestration"] DEPLOY["ModelServer"] LOCALRUN["Orchestrator"] SLURMGEN["SlurmGen"] end subgraph EXEC["Executors"] LOCAL["LocalExecutor"] DOCKER["DockerExecutor"] SLURM["SlurmExecutor"] end subgraph SAND["Sandbox"] SBASE["Sandbox Protocol"] SDOCK["DockerSandbox"] SSLURM["SlurmSandbox"] SLOCAL["LocalSandbox"] SECS["ECSFargateSandbox"] SMGR["SandboxManager"] end subgraph PROXY["Proxy"] LITELLM["LiteLLM Proxy"] INTERCEPT["Interceptors"] end subgraph OBS["Observability"] TYPES["StepRecord
ModelResponse"] COLLECT["ArtifactCollector"] PROG["ConsoleProgress"] end subgraph SCORE["Scoring"] JUDGE["judge.py"] JSCHEMA["json_schema.py"] end RUN --> EVALCFG RUN --> EXEC EXEC --> ORCH ORCH --> DEPLOY ORCH --> LOOP LOOP --> SMGR SERVE --> REG VALIDATE --> LOOP LOOP --> REG LOOP --> SOLVER SOLVER --> MC LOOP --> COLLECT LOOP --> PROG REG --> BYOB REG --> GYM REG --> SKILLS REG --> LMEVAL REG --> VLM LOOP --> JUDGE LOOP --> LITELLM LITELLM --> INTERCEPT style CLI fill:#e8eaf6 style ENVS fill:#e8f5e9 style ENGINE fill:#fff3e0 style CFG fill:#e8eaf6 style ORCH fill:#fff8e1 style EXEC fill:#f3e5f5 style OBS fill:#fce4ec style SAND fill:#e0f7fa style SCORE fill:#fff9c4 style PROXY fill:#e1f5fe ``` ## Package Map | Package | Responsibility | Key types | | ---------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `environments/` | Base class, registry, `@benchmark` BYOB API, environment types | `EvalEnvironment`, `SeedResult`, `VerifyResult`, `BenchmarkDefinition` | | `benchmarks/` | 17 built-in benchmarks (`@benchmark` + `@scorer` or `@register`) | Scorer functions | | `solvers/` | Pluggable inference strategies | `Solver`, `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 plugins | `run_evaluation()`, `ModelClient`, `CheckpointManager`, `InspectExporter`, `WandBExporter` | | `config/` | Pydantic config schemas, env expansion, YAML composition | `EvalConfig`, `parse_eval_config()`, `compose_config()`, `services.py`, `sandboxes.py`, `solvers.py`, `scoring.py`, `clusters.py` | | `orchestration/` | Suite orchestration, model server management, SLURM generation, proxy lifecycle | `run_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 patterns | `Sandbox`, `SandboxSpec`, `SandboxManager`, `ECSFargateSandbox`, `NoSandbox`, `StatefulSandbox`, `StatelessSandbox` | | `interceptors/` | LiteLLM proxy callback plugins for request/response interception | `resolve_interceptors()`, `BaseInterceptor` | | `scoring/` | Verification scorers, judge pipeline, JSON schema | `exact_match`, `code_sandbox`, `needs_judge` | | `observability/` | Rich telemetry capture | `StepRecord`, `ModelResponse`, `RuntimeStats`, `ArtifactCollector` | | `metrics/` | Statistical aggregation | `pass_at_k()`, `bootstrap_ci()`, `category_breakdown()` | | `cli/` | CLI commands | `nel 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: ```mermaid classDiagram class EvalEnvironment { <> +str name +seed(idx) SeedResult +verify(response, expected, sandbox?) VerifyResult +sandbox_specs() list~SandboxSpec~ | None +dataset_size() int +close() } class ByobEnvironment { +BenchmarkDefinition definition } class GymEnvironment { +str endpoint } class SkillsEnvironment { +str benchmark +str eval_type } class LMEvalEnvironment { +str task_name } class VLMEvalKitEnvironment { +str dataset_name } class ContainerEnvironment { +str image_uri +str task_name } EvalEnvironment <|-- ByobEnvironment EvalEnvironment <|-- GymEnvironment EvalEnvironment <|-- SkillsEnvironment EvalEnvironment <|-- LMEvalEnvironment EvalEnvironment <|-- VLMEvalKitEnvironment EvalEnvironment <|-- ContainerEnvironment ``` ### Resolution The registry resolves environment names in order: 1. **URI scheme** -- `lm-eval://task`, `skills://name`, `gym://host:port`, `gym://name`, `vlmevalkit://dataset`, `container://image#task` 2. **Built-in registry** -- names registered via `@benchmark` or `@register` ```python from nemo_evaluator import get_environment env = get_environment("mmlu") # built-in env = get_environment("lm-eval://aime25") # lm-eval task env = get_environment("skills://gpqa") # NeMo Skills env = get_environment("gym://localhost:9090") # remote Gym env = get_environment("vlmevalkit://MMBench_DEV_EN") # VLMEvalKit ``` ## Evaluation Flow ```mermaid sequenceDiagram participant CLI as nel eval run participant Exec as Executor participant Runner as Orchestrator participant Deploy as ModelServer participant Loop as eval_loop participant Solver as Solver participant Env as Environment participant Obs as ArtifactCollector CLI->>Exec: executor.run(config) Exec->>Runner: run_local(config) Runner->>Deploy: start() Deploy-->>Runner: model_url opt proxy configured Runner->>Runner: start_proxy(model_url) Note right of Runner: model_url rewritten to proxy URL end Runner->>Loop: run_evaluation(env, solver, config, sandbox_manager) loop For each problem x repeat Loop->>Env: seed(idx) Env-->>Loop: SeedResult(prompt, expected, sandbox_spec?) opt sandbox configured Loop->>Loop: manager.acquire(spec) end Loop->>Solver: solve(task, sandbox?) Solver-->>Loop: SolveResult(response) Loop->>Env: verify(response, expected, sandbox?) Env-->>Loop: VerifyResult(reward, details) opt sandbox acquired Loop->>Loop: manager.release(sandbox) end Loop->>Obs: record(StepRecord) end Loop-->>Runner: bundle Runner->>Deploy: stop() ``` ## 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. | Solver | Config `type` | Protocol | Use case | | ---------------- | ----------------- | ---------------------------- | -------------------------------------------------------------------- | | `ChatSolver` | `simple` | `/chat/completions` | Standard benchmarks (default) | | `VLMSolver` | `simple` (images) | `/chat/completions` + images | Vision-language benchmarks | | `HarborSolver` | `harbor` | Harbor agent SDK | Agentic evaluation (OpenHands, SWE-agent) | | `ReActSolver` | `tool_calling` | NEL-driven ReAct loop | Full-observability tool use (Gym HTTP tools, sandbox tools, or both) | | `GymSolver` | `gym_delegation` | HTTP to nemo-gym | Delegate solve to gym server | | `NatSolver` | (via service) | SSE `/generate/full` | NAT agent benchmarks | | `OpenClawSolver` | `openclaw` | OpenClaw CLI | OpenClaw benchmarks | ```python class Solver(Protocol): async def solve(self, task: SeedResult) -> SolveResult: ... ``` ## Executor Protocol All execution backends implement the `Executor` protocol (`executors/__init__.py`): ```python class Executor(Protocol): name: str def run(self, config, *, dry_run=False, resume=False, background=False, submit=False) -> None: ... def status(self, output_dir) -> ProcessState: ... def stop(self, output_dir) -> bool: ... @staticmethod def detect(output_dir) -> bool: ... ``` The CLI dispatches via `get_executor(config.cluster.type)` and `detect_executor(output_dir)` -- no if/elif trees. | Executor | Config | Metadata file | What it does | | ---------------- | ---------------------- | ---------------- | ------------------------------------------------------------------------------------ | | `LocalExecutor` | `cluster.type: local` | `nel.pid` | In-process eval with optional model deployment, checkpointing, and failure isolation | | `DockerExecutor` | `cluster.type: docker` | `docker.json` | Runs eval inside a Docker container with the correct per-harness image | | `SlurmExecutor` | `cluster.type: slurm` | `slurm_job.json` | Generates 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. ```bash # 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 `type` | Internal class | Description | | ------------- | ------------------------ | ---------------------------------- | | `api` | `APIDeployment` | External API, no server management | | `vllm` | `ProcessModelDeployment` | Local vLLM process | | `sglang` | `ProcessModelDeployment` | Local SGLang process | | `trt_llm` | `ProcessModelDeployment` | Local TensorRT-LLM process | All deployments implement `start() -> url`, `health_wait()`, and `stop()`. ## Observability Data Model ```mermaid classDiagram class StepRecord { +str step_id +int problem_idx +int repeat +str prompt +str expected_answer +ModelResponse model_response +float reward +str extracted_answer +str scoring_method +dict scoring_details +float seed_ms +float model_ms +float verify_ms +str failure_category } class ModelResponse { +str content +str model +str finish_reason +int prompt_tokens +int completion_tokens +int reasoning_tokens +float latency_ms +dict raw_response } class RuntimeStats { +int total_steps +int total_tokens +float elapsed_seconds +dict latency_percentiles_ms +float tokens_per_second +float steps_per_second +int model_errors } class FailureReport { +int total_failures +float failure_rate +dict categories +list exemplars } StepRecord --> ModelResponse ``` ## Resilience and Resume Multi-benchmark suites use `CheckpointManager` to track per-benchmark completion: ```mermaid flowchart LR START["nel eval run suite.yaml"] --> B1["Benchmark 1"] B1 -->|"completed"| CKPT1["checkpoint: done"] CKPT1 --> B2["Benchmark 2"] B2 -->|"FAILED"| CKPT2["checkpoint: failed"] CKPT2 --> B3["Benchmark 3"] B3 -->|"completed"| CKPT3["checkpoint: done"] CKPT3 --> SUMMARY["Summary: 2 done, 1 failed"] SUMMARY --> RESUME["nel eval run suite.yaml --resume"] RESUME --> B2R["Retry Benchmark 2"] ``` * **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 ```mermaid flowchart LR subgraph "Environment Variables" IDX["NEL_SHARD_IDX
or SLURM_ARRAY_TASK_ID"] TOT["NEL_TOTAL_SHARDS
or SLURM_ARRAY_TASK_COUNT"] end IDX --> RANGE["get_shard_range(total, idx, shards)"] TOT --> RANGE RANGE --> LOOP["run_evaluation(..., problem_range=(start, end))"] LOOP --> OUTPUT["shard_N/results.jsonl"] OUTPUT --> MERGE["merge_results(shard_dirs)"] MERGE --> FINAL["merged/eval-*.json
Recomputed pass@k, CI"] ``` Sharding is transparent to the eval loop. `problem_range` changes the iteration bounds. The merge step recomputes global metrics from concatenated per-shard results.