feat(evaluator): unify dataset + task evaluation under one Evaluator - #736
feat(evaluator): unify dataset + task evaluation under one Evaluator#736SandyChapman wants to merge 1 commit into
Conversation
|
| return namespace_result(metric_key, result, aggregate_fields) | ||
|
|
||
| async def evaluate( | ||
| async def evaluate_metric( |
There was a problem hiding this comment.
for consistency, should it be evaluate_datasets?
| Raises: | ||
| ValueError: If both ``inference_fn`` and ``agent_inference_fn_factory`` are set. | ||
| """ | ||
| if inference_fn is not None and agent_inference_fn_factory is not None: |
There was a problem hiding this comment.
also need a check that only one or the other is provided
| tasks: Tasks to evaluate; each task carries its own metrics. | ||
| trials: Precomputed trials to score. Mutually exclusive with ``target``. |
There was a problem hiding this comment.
considering tasks and trials are mutually exclusive, we should expose this, so it's clear before invoking it
| raise TypeError("metrics must be a Metric or a sequence of Metric objects") | ||
| return await self._backend.evaluate( | ||
| metric=metrics, | ||
| async def run( |
There was a problem hiding this comment.
should probably become run_dataset_eval since it's specific to that path
|
|
||
| from nemo_evaluator_sdk.execution.evaluator import Evaluator | ||
|
|
||
| evaluator = Evaluator(client.evaluator.as_backend()) |
There was a problem hiding this comment.
are we going to allow sdk to be an interface with platform again via injected backend? At some point in the past (~April), we reverted this change
There was a problem hiding this comment.
We should discuss today if we can find time, otherwise Monday. I'm not sure why we wouldn't support this as it aligns with the early design we had to make moving from SDK to Plugin as straight-forward as possible.
|
Some initial thoughts:
|
ef32395 to
e28965b
Compare
…luator Merges the two SDK entrypoints — dataset-driven `Evaluator` and task-driven `AgentEvaluator` — into a single `Evaluator` whose methods map 1:1 onto the backend protocol, so in-process vs platform and dataset vs agent evaluation differ only by which backend you inject. `Evaluator` (public): - `run_dataset_eval` — metrics over a dataset → backend `evaluate_dataset` - `run_taskset_eval` — tasks carrying their own metrics → backend `evaluate_taskset` - each has a `_sync` twin; `run`/`run_sync` remain as dataset dispatchers. `client.evaluator.as_backend()` returns the plugin executor, so `Evaluator(NeMoPlatform(...).evaluator.as_backend())` runs both dataset and taskset evaluation on the platform. `AgentEvaluator` becomes a deprecated shim and the agent-eval job runner moves onto the unified path. Submit live runners over the wire --------------------------------- A runner built in-process previously had no wire form, so a Codex/Fabric runtime could only be evaluated locally. `jobs/runner_targets.py` adds the inverse of `_resolve_target`: it maps a live runner onto the target spec that reproduces it job-side. Both directions are platform concerns, so both live in the plugin — the SDK runtimes know nothing about job specs, target kinds, or the wire. They expose their own configuration as ordinary read-only attributes, and the plugin decides how that is spelled. Environment-supplied arguments (work roots, sandbox providers, process factories) never travel; the job supplies its own. State with no wire representation — injected skills, a custom `base_dir` or `codex_bin` — raises `UnsubmittableRunnerError` instead of being dropped, so a submitted run never silently differs from the local one it mirrors. Resolved secret values never enter a spec; only references do. Fabric container runner reaches the job path -------------------------------------------- `FabricRunnerTarget` gains `sandbox` (`FabricSandboxSpec`: provider, image, secrets). Setting it swaps the host `FabricAgentRuntime` for `FabricContainerRuntime`, whose evidence contract is a superset. Fields the sandboxed runtime cannot honor (`model`, non-default `timeout_s`, `capture_trajectory=False`) are rejected rather than ignored. The job compiler now surfaces a sandboxed runner's harness credentials as `from_secret` task environment, alongside metric and endpoint secrets. The plugin backend is remote ---------------------------- Both protocol methods now submit to the platform and read the completed result back from job artifacts; agent-eval jobs post to their own `agent-evaluate` collection, which required parameterizing the job-collection segment in the URL helpers and adding `AgentEvaluatorJob` (agent-eval payloads do not validate as `BaseJob[EvaluateSpec]`). Dataset results are read from the job's own result JSON because it is the one representation covering both the single-metric and benchmark shapes. The "use the plugin but run in-process" mode is removed rather than renamed — in-process evaluation is what `Evaluator()`'s default `LocalBackend` is for. That deletes `run_local` on both executors, their local spec resolvers, and `sdk/fs_utils.py`, whose only purpose was reading local job results off disk. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Sandy Chapman <schapman@nvidia.com>
e28965b to
33a1eb9
Compare
Draft — seeking team input on the evaluator SDK UX + naming before I polish.
What this does
Merges the two SDK entrypoints — dataset-driven
Evaluatorand task-drivenAgentEvaluator— into a singleEvaluatorwhose methods map 1:1 onto the backend protocol, so in-process ↔ platform and dataset ↔ agent eval differ only by which backend you inject.Agree, one entrypoint:
Evaluator.submit(target=model|agent|runner, taskset=[...]) # agent eval
Evaluator.submit(target=model|agent|runner, taskset=generate_taskset(metrics, dataset))
Evaluator(public):run_dataset_eval— metrics over a dataset → backendevaluate_datasetrun_taskset_eval— tasks (each carrying its own metrics) → backendevaluate_taskset_synctwin;run/run_syncremain as backward-compatible dataset dispatchers.Backend protocol (
EvaluationBackend/SyncEvaluationBackend):evaluate_dataset/evaluate_taskset.Plugin: the executor implements the full protocol;
client.evaluator.as_backend()returns it, soEvaluator(NeMoPlatform(...).evaluator)runs both dataset and taskset eval through the platform.AgentEvaluatorbecomes a thin deprecated shim; the agent-eval job runner is migrated onto the unified path.Submitting a live runner
A runner built in-process previously had no wire form at all, so a Codex or Fabric runtime could only ever be evaluated locally —
_agent_eval_target_to_specrejected it outright.jobs/runner_targets.pyadds the inverse of_resolve_target: it maps a live runner onto the target spec that reproduces it job-side. Both directions are platform concerns, so both live in the plugin — the SDK runtimes know nothing about job specs, target kinds, or the wire. They expose their own configuration as ordinary read-only attributes; the plugin decides how that is spelled.Environment-supplied arguments (work roots, sandbox providers, process factories) never travel — the job supplies its own. State with no wire representation — injected skills, a custom
base_dirorcodex_bin— raisesUnsubmittableRunnerErrorrather than being dropped, so a submitted run never silently differs from the local one it claims to mirror. Resolved secret values never enter a spec; only references do.Fabric container runner on the job path
FabricRunnerTargetgainssandbox(FabricSandboxSpec: provider / image / secrets). Setting it swaps the hostFabricAgentRuntimeforFabricContainerRuntime, whose evidence contract is a superset (workspaceandlogsalways captured). Fields the sandboxed runtime can't honor —model, non-defaulttimeout_s,capture_trajectory=False— are rejected rather than ignored. The job compiler surfaces a sandboxed runner's harness credentials asfrom_secrettask environment, alongside metric and endpoint secrets.The plugin backend is remote
Both protocol methods now submit to the platform and read the completed result back from job artifacts. Agent-eval jobs post to their own
agent-evaluatecollection, which meant parameterizing the job-collection segment in the URL helpers and addingAgentEvaluatorJob(agent-eval payloads don't validate asBaseJob[EvaluateSpec]). Dataset results come from the job's own result JSON — the one representation covering both the single-metric and benchmark shapes.The "use the plugin but run in-process" mode is removed rather than renamed: in-process evaluation is what
Evaluator()'s defaultLocalBackendis for. That deletesrun_localon both executors, their local spec resolvers, andsdk/fs_utils.py, whose only purpose was reading local job results off disk.Questions for reviewers
run_dataset_eval/run_taskset_evalonEvaluator, mirroringevaluate_dataset/evaluate_taskseton the backend. Good alignment, or too verbose?Evaluator(client=NeMoPlatform(...).evaluator)work without the.as_backend()hop (i.e. make the resource itself satisfy the protocol)? Deliberately deferred — a guard test currently keepsevaluate*off the resource surface._synctwins +SyncEvaluationBackend, or go async-only? (Leaning keep: notebook + job-runner callers, andrun_syncis Jupyter-safe.)AgentEvaluatoras a warning-emitting shim →Evaluator().run_taskset_eval— path OK?evaluate_remote— now redundant withevaluate_dataset(same submit; it differs only in being single-metric and using the dedicated aggregate/row-score routes) and has no callers left insrc. Remove it, or keep for the narrower result-fetch?Known gaps (follow-ups)
AgentSkillis a set of local directories, so a skill-bearing runner raises on submit. Carrying them needs a fileset upload — the same class of problem cloudpickle solves for metrics. A/B skill evals stay local until then.env_namefrom platform secretsecret_name, thenLocalSecretResolverlooks up names derived fromsecret_name— these only agree when the two match. Pre-existing across the metric path; the runner path now inherits it.submit(fire-and-forget returning a job handle, assubmitdoes for datasets) and result-persistence for the new runner targets.🤖 Generated with Claude Code