Skip to content

Evaluation

xaytune provides two evaluation paths: custom dataset evaluation with evaluate() and benchmark evaluation with benchmark_evaluate().

evaluate()

Evaluate a model on a custom dataset with configurable metrics.

from xaytune.eval import evaluate

results = evaluate(
    model="output/my-finetune",
    dataset=[{"input_ids": ..., "labels": ...}],
    metrics=["loss", "perplexity"],
)

print(results)
# {'loss': 1.234, 'perplexity': 3.435}

Function Signature

def evaluate(
    *,
    model: Any,
    dataset: list[dict[str, Any]],
    metrics: list[str] | None = None,
) -> dict[str, float]:
Parameter Type Default Description
model model object or str required A model instance or path to load from
dataset list[dict] required List of data batches to evaluate on
metrics list[str] | None ["loss", "perplexity"] Metric names to compute (must be in metric_registry)

Returns: dict[str, float] mapping metric names to their computed values.

Note

When model is a string path, xaytune automatically loads the model and tokenizer using xaytune.models.load_model().


benchmark_evaluate()

Run standard benchmarks using lm-eval.

from xaytune.eval.benchmarks import benchmark_evaluate

results = benchmark_evaluate(
    model="meta-llama/Llama-3.1-8B",
    benchmarks=["mmlu", "gsm8k", "hellaswag"],
    num_fewshot=5,
)

for task, metrics in results.items():
    print(f"{task}: {metrics}")

Function Signature

def benchmark_evaluate(
    *,
    model: str,
    benchmarks: list[str],
    num_fewshot: int | None = None,
) -> dict[str, dict[str, Any]]:
Parameter Type Default Description
model str required Model path or Hugging Face Hub name
benchmarks list[str] required List of benchmark task names
num_fewshot int | None None Number of few-shot examples (benchmark default if None)

Returns: Nested dict {task_name: {metric_name: value}}.

Requires lm-eval

Install the eval extra to use benchmarks:

pip install xaytune[eval]


Built-in Metrics

xaytune ships three metrics, registered in xaytune.eval.metrics.metric_registry:

Metric Function Description
loss compute_loss Average cross-entropy loss
perplexity compute_perplexity Exponentiated average loss: exp(mean_loss)
token_accuracy compute_token_accuracy Fraction of correctly predicted tokens

Custom Metrics

Register your own metrics with the @register_metric decorator:

from xaytune.eval.metrics import register_metric

@register_metric("bleu")
def compute_bleu(predictions, references, **kwargs):
    # Your BLEU implementation here
    ...
    return score

Once registered, custom metrics can be used anywhere metrics are accepted:

results = evaluate(model=model, dataset=data, metrics=["loss", "bleu"])

Or in YAML config:

eval:
  metrics: [loss, perplexity, bleu]

CLI Usage

Benchmark Evaluation

xaytune eval --model output/my-finetune --benchmarks mmlu,gsm8k --num-fewshot 5

Dataset Evaluation

xaytune eval --model output/my-finetune --dataset data/eval.jsonl --metrics loss,perplexity

Model Comparison

Compare two models side-by-side on the same benchmarks:

xaytune compare model-a model-b --benchmarks mmlu,gsm8k

This prints a table showing each model's score on every benchmark metric.


Agent Evaluation

Evaluate agent performance on tool-use tasks with evaluate_agent(). Scores a dataset of prompt-response pairs across four metrics.

from xaytune.eval.agent_metrics import evaluate_agent

results = evaluate_agent(
    responses=[
        {"prompt": "Search for cats", "response": "<tool_call>..."},
    ],
    expected_tools=["search"],
    success_markers=["Done"],
    max_steps=5,
)
# {'tool_use_accuracy': 0.85, 'task_success_rate': 0.90, ...}

Agent Metrics

Metric Description
tool_use_accuracy Fraction of samples where the correct tools were called
task_success_rate Fraction of samples that completed the task
step_efficiency Average efficiency score (fewer tool calls = higher)
error_recovery_rate Fraction of samples that recovered after tool errors

evaluate_agent(responses, *, expected_tools=None, required_args=None, success_markers=None, failure_markers=None, max_steps=10, optimal_steps=None, error_indicators=None, parser=None)

Source code in xaytune/eval/agent_metrics.py
def evaluate_agent(
    responses: list[dict[str, str]],
    *,
    expected_tools: list[str] | None = None,
    required_args: dict[str, list[str]] | None = None,
    success_markers: list[str] | None = None,
    failure_markers: list[str] | None = None,
    max_steps: int = 10,
    optimal_steps: int | None = None,
    error_indicators: list[str] | None = None,
    parser: Callable | None = None,
) -> dict[str, float]:
    return {
        "tool_use_accuracy": compute_tool_use_accuracy(
            responses,
            expected_tools=expected_tools,
            required_args=required_args,
            parser=parser,
        ),
        "task_success_rate": compute_task_success_rate(
            responses,
            success_markers=success_markers,
            failure_markers=failure_markers,
            parser=parser,
        ),
        "step_efficiency": compute_step_efficiency(
            responses,
            max_steps=max_steps,
            optimal_steps=optimal_steps,
            parser=parser,
        ),
        "error_recovery_rate": compute_error_recovery_rate(
            responses,
            error_indicators=error_indicators,
            parser=parser,
        ),
    }

compute_tool_use_accuracy(responses, *args, expected_tools=None, required_args=None, parser=None, **kwargs)

Source code in xaytune/eval/agent_metrics.py
@register_metric("tool_use_accuracy")
def compute_tool_use_accuracy(
    responses: list[dict[str, str]],
    *args: Any,
    expected_tools: list[str] | None = None,
    required_args: dict[str, list[str]] | None = None,
    parser: Callable | None = None,
    **kwargs: Any,
) -> float:
    if not responses:
        return 0.0
    scores = []
    for item in responses:
        score = tool_use_quality_reward(
            item.get("prompt", ""),
            item.get("response", ""),
            expected_tools=expected_tools,
            required_args=required_args,
            parser=parser,
        )
        scores.append(score)
    return sum(scores) / len(scores)

compute_task_success_rate(responses, *args, success_markers=None, failure_markers=None, parser=None, **kwargs)

Source code in xaytune/eval/agent_metrics.py
@register_metric("task_success_rate")
def compute_task_success_rate(
    responses: list[dict[str, str]],
    *args: Any,
    success_markers: list[str] | None = None,
    failure_markers: list[str] | None = None,
    parser: Callable | None = None,
    **kwargs: Any,
) -> float:
    if not responses:
        return 0.0
    successes = 0
    for item in responses:
        score = task_completion_reward(
            item.get("prompt", ""),
            item.get("response", ""),
            success_markers=success_markers,
            failure_markers=failure_markers,
            parser=parser,
        )
        if score >= 0.5:
            successes += 1
    return successes / len(responses)

compute_step_efficiency(responses, *args, max_steps=10, optimal_steps=None, parser=None, **kwargs)

Source code in xaytune/eval/agent_metrics.py
@register_metric("step_efficiency")
def compute_step_efficiency(
    responses: list[dict[str, str]],
    *args: Any,
    max_steps: int = 10,
    optimal_steps: int | None = None,
    parser: Callable | None = None,
    **kwargs: Any,
) -> float:
    if not responses:
        return 0.0
    scores = []
    for item in responses:
        score = efficiency_reward(
            item.get("prompt", ""),
            item.get("response", ""),
            max_steps=max_steps,
            optimal_steps=optimal_steps,
            parser=parser,
        )
        scores.append(score)
    return sum(scores) / len(scores)

compute_error_recovery_rate(responses, *args, error_indicators=None, parser=None, **kwargs)

Source code in xaytune/eval/agent_metrics.py
@register_metric("error_recovery_rate")
def compute_error_recovery_rate(
    responses: list[dict[str, str]],
    *args: Any,
    error_indicators: list[str] | None = None,
    parser: Callable | None = None,
    **kwargs: Any,
) -> float:
    if not responses:
        return 0.0
    if error_indicators is None:
        error_indicators = ["error", "Error", "ERROR", "failed", "Failed", "exception"]

    recovered = 0
    total_with_errors = 0

    for item in responses:
        response = item.get("response", "")
        has_error = any(indicator in response for indicator in error_indicators)
        if not has_error:
            continue
        total_with_errors += 1
        calls = parse_tool_calls(response, parser=parser)
        if not calls:
            continue
        last_error_pos = max(response.rfind(ind) for ind in error_indicators if ind in response)
        last_call_pos = response.rfind("</tool_call>")
        has_final_answer = response.rfind("</tool_result>")
        text_after = (
            response[has_final_answer + len("</tool_result>") :].strip()
            if has_final_answer != -1
            else ""
        )

        if last_call_pos > last_error_pos or text_after:
            recovered += 1

    if total_with_errors == 0:
        return 1.0
    return recovered / total_with_errors

Full API Reference

evaluate(*, model, dataset, metrics=None)

Evaluate a model on a list of batches and compute metrics.

Parameters:

Name Type Description Default
model Any

A model instance or HuggingFace model name string.

required
dataset list[dict[str, Any]]

List of batch dicts (each passable to model(**batch)).

required
metrics list[str] | None

Metric names to compute (default: ["loss", "perplexity"]).

None

Returns:

Type Description
dict[str, float]

Dict mapping metric names to computed values.

Source code in xaytune/eval/evaluate.py
def evaluate(
    *,
    model: Any,
    dataset: list[dict[str, Any]],
    metrics: list[str] | None = None,
) -> dict[str, float]:
    """Evaluate a model on a list of batches and compute metrics.

    Args:
        model: A model instance or HuggingFace model name string.
        dataset: List of batch dicts (each passable to ``model(**batch)``).
        metrics: Metric names to compute (default: ``["loss", "perplexity"]``).

    Returns:
        Dict mapping metric names to computed values.
    """
    if metrics is None:
        metrics = ["loss", "perplexity"]

    if isinstance(model, str):
        from xaytune.models import load_model

        model_result = load_model(model)
        model = model_result.model

    device = next(model.parameters()).device

    losses: list[float] = []
    all_predictions: list[int] = []
    all_references: list[int] = []

    if hasattr(model, "eval"):
        model.eval()

    with torch.no_grad():
        for batch in dataset:
            if isinstance(batch, dict):
                batch = {
                    k: v.to(device) if isinstance(v, torch.Tensor) else v
                    for k, v in batch.items()
                }
                outputs = model(**batch)
            else:
                outputs = model(batch)

            if hasattr(outputs, "loss") and outputs.loss is not None:
                losses.append(outputs.loss.item())

            if (
                hasattr(outputs, "logits")
                and isinstance(batch, dict)
                and "labels" in batch
            ):
                preds = outputs.logits.argmax(dim=-1)
                labels = batch["labels"]
                mask = labels != -100
                all_predictions.extend(preds[mask].cpu().tolist())
                all_references.extend(labels[mask].cpu().tolist())

    results: dict[str, float] = {}
    for metric_name in metrics:
        compute_fn = metric_registry.get(metric_name)
        if metric_name in ("loss", "perplexity"):
            results[metric_name] = compute_fn(losses)
        else:
            results[metric_name] = compute_fn(all_predictions, all_references)

    return results

benchmark_evaluate(*, model, benchmarks, num_fewshot=None)

Run lm-eval-harness benchmarks against a HuggingFace model.

Parameters:

Name Type Description Default
model str

HuggingFace model name or local path.

required
benchmarks list[str]

Benchmark task names (e.g. ["mmlu", "gsm8k"]).

required
num_fewshot int | None

Number of few-shot examples. None uses each benchmark's default.

None

Returns:

Type Description
dict[str, dict[str, Any]]

Dict mapping benchmark names to their result dicts.

Raises:

Type Description
ImportError

If lm-eval is not installed.

Source code in xaytune/eval/benchmarks.py
def benchmark_evaluate(
    *,
    model: str,
    benchmarks: list[str],
    num_fewshot: int | None = None,
) -> dict[str, dict[str, Any]]:
    """Run lm-eval-harness benchmarks against a HuggingFace model.

    Args:
        model: HuggingFace model name or local path.
        benchmarks: Benchmark task names (e.g. ``["mmlu", "gsm8k"]``).
        num_fewshot: Number of few-shot examples. ``None`` uses each
            benchmark's default.

    Returns:
        Dict mapping benchmark names to their result dicts.

    Raises:
        ImportError: If ``lm-eval`` is not installed.
    """
    if lm_eval is None:
        raise ImportError(
            "lm-eval is required for benchmark evaluation. "
            "Install it with: pip install xaytune[eval]"
        )

    kwargs: dict[str, Any] = {
        "model": "hf",
        "model_args": f"pretrained={model}",
        "tasks": benchmarks,
    }
    if num_fewshot is not None:
        kwargs["num_fewshot"] = num_fewshot

    raw = lm_eval.simple_evaluate(**kwargs)

    return raw.get("results", {})  # type: ignore[no-any-return]

compute_loss(losses, *args, **kwargs)

Compute mean loss across batches.

Source code in xaytune/eval/metrics.py
@register_metric("loss")
def compute_loss(losses: list[float], *args: Any, **kwargs: Any) -> float:
    """Compute mean loss across batches."""
    if not losses:
        return 0.0
    return sum(losses) / len(losses)

compute_perplexity(losses, *args, **kwargs)

Compute perplexity as exp(mean_loss).

Source code in xaytune/eval/metrics.py
@register_metric("perplexity")
def compute_perplexity(losses: list[float], *args: Any, **kwargs: Any) -> float:
    """Compute perplexity as ``exp(mean_loss)``."""
    if not losses:
        return 0.0
    mean_loss = sum(losses) / len(losses)
    return math.exp(mean_loss)

compute_token_accuracy(predictions, references, *args, **kwargs)

Compute fraction of tokens where prediction matches reference.

Source code in xaytune/eval/metrics.py
@register_metric("token_accuracy")
def compute_token_accuracy(
    predictions: list[int],
    references: list[int],
    *args: Any,
    **kwargs: Any,
) -> float:
    """Compute fraction of tokens where prediction matches reference."""
    if not predictions:
        return 0.0
    correct = sum(p == r for p, r in zip(predictions, references))
    return correct / len(predictions)