Skip to content

Commit 6aaedc5

Browse files
feature: add --num-prompts option to limit number of prompts used in CLI and tests
1 parent c32af1d commit 6aaedc5

3 files changed

Lines changed: 127 additions & 7 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ select = ["E", "F", "I", "N", "W", "UP", "B", "SIM"]
7676
"html.py" = ["E501"]
7777

7878
[tool.bandit]
79-
targets= ["src"]
79+
targets = ["src"]
80+
exclude_dirs = ["tests"]
8081

8182
[tool.mypy]
8283
python_version = "3.11"

src/infer_check/cli.py

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ def common_options(f: F) -> F:
2727
type=click.IntRange(min=1, clamp=True),
2828
help="Override default max tokens for generation.",
2929
),
30+
click.option(
31+
"--num-prompts",
32+
default=None,
33+
type=click.IntRange(min=1, clamp=True),
34+
help="Limit number of prompts to use.",
35+
),
3036
]
3137
for option in reversed(options):
3238
f = option(f)
@@ -51,11 +57,18 @@ def _resolve_prompts(prompts: str) -> Path:
5157
show_default=True,
5258
help="Default max tokens for generation (applies to all prompts unless they specify their own).",
5359
)
60+
@click.option(
61+
"--num-prompts",
62+
default=None,
63+
type=click.IntRange(min=1, clamp=True),
64+
help="Default number of prompts to use from a suite.",
65+
)
5466
@click.pass_context
55-
def main(ctx: click.Context, max_tokens: int) -> None:
67+
def main(ctx: click.Context, max_tokens: int, num_prompts: int | None) -> None:
5668
"""infer-check: correctness and reliability testing for LLM inference engines."""
5769
ctx.ensure_object(dict)
5870
ctx.obj["max_tokens"] = max_tokens
71+
ctx.obj["num_prompts"] = num_prompts
5972

6073

6174
# ---------------------------------------------------------------------------
@@ -103,6 +116,7 @@ def sweep(
103116
baseline: str | None,
104117
base_url: str | None,
105118
max_tokens: int | None,
119+
num_prompts: int | None,
106120
) -> None:
107121
"""Run a quantization sweep: compare pre-quantized models against a baseline.
108122
@@ -121,9 +135,11 @@ def sweep(
121135
from infer_check.runner import TestRunner
122136
from infer_check.suites.loader import load_suite
123137

124-
# Update max_tokens from subcommand if provided
138+
# Update defaults from subcommand if provided
125139
if max_tokens is not None:
126140
ctx.obj["max_tokens"] = max_tokens
141+
if num_prompts is not None:
142+
ctx.obj["num_prompts"] = num_prompts
127143

128144
# Parse label=model_path pairs
129145
model_map: dict[str, str] = {}
@@ -153,6 +169,9 @@ def sweep(
153169
tag = " (baseline)" if label == baseline_label else ""
154170
console.print(f" {label}: {path}{tag}")
155171
prompt_list = load_suite(_resolve_prompts(prompts))
172+
if ctx.obj["num_prompts"] is not None:
173+
prompt_list = prompt_list[: ctx.obj["num_prompts"]]
174+
156175
# Apply global max_tokens only if not explicitly set in the prompt JSONL
157176
for p in prompt_list:
158177
if "max_tokens" not in p.model_fields_set:
@@ -308,6 +327,7 @@ def compare(
308327
label_b: str | None,
309328
report: bool,
310329
max_tokens: int | None,
330+
num_prompts: int | None,
311331
) -> None:
312332
"""Compare two quantizations of the same model.
313333
@@ -337,9 +357,11 @@ def compare(
337357
from infer_check.runner import TestRunner
338358
from infer_check.suites.loader import load_suite
339359

340-
# Update max_tokens from subcommand if provided
360+
# Update defaults from subcommand if provided
341361
if max_tokens is not None:
342362
ctx.obj["max_tokens"] = max_tokens
363+
if num_prompts is not None:
364+
ctx.obj["num_prompts"] = num_prompts
343365

344366
resolved_a = resolve_model(model_a, base_url=base_url, label=label_a)
345367
resolved_b = resolve_model(model_b, base_url=base_url, label=label_b)
@@ -351,6 +373,9 @@ def compare(
351373
)
352374

353375
prompt_list = load_suite(_resolve_prompts(prompts))
376+
if ctx.obj["num_prompts"] is not None:
377+
prompt_list = prompt_list[: ctx.obj["num_prompts"]]
378+
354379
# Apply global max_tokens only if not explicitly set in the prompt JSONL
355380
for p in prompt_list:
356381
if "max_tokens" not in p.model_fields_set:
@@ -573,15 +598,18 @@ def diff(
573598
base_urls: str | None,
574599
chat: bool,
575600
max_tokens: int | None,
601+
num_prompts: int | None,
576602
) -> None:
577603
"""Compare outputs across different backends for the same model and prompts."""
578604
from infer_check.backends.base import BackendConfig, get_backend
579605
from infer_check.runner import TestRunner
580606
from infer_check.suites.loader import load_suite
581607

582-
# Update max_tokens from subcommand if provided
608+
# Update defaults from subcommand if provided
583609
if max_tokens is not None:
584610
ctx.obj["max_tokens"] = max_tokens
611+
if num_prompts is not None:
612+
ctx.obj["num_prompts"] = num_prompts
585613

586614
backend_names = [b.strip() for b in backends.split(",") if b.strip()]
587615
url_list: list[str | None] = [u.strip() for u in base_urls.split(",")] if base_urls else [None] * len(backend_names)
@@ -592,6 +620,9 @@ def diff(
592620
console.print(f"[bold cyan]diff[/bold cyan] model={model} backends={backend_names} quant={quant}")
593621

594622
prompt_list = load_suite(_resolve_prompts(prompts))
623+
if ctx.obj["num_prompts"] is not None:
624+
prompt_list = prompt_list[: ctx.obj["num_prompts"]]
625+
595626
# Apply global max_tokens only if not explicitly set in the prompt JSONL
596627
for p in prompt_list:
597628
if "max_tokens" not in p.model_fields_set:
@@ -693,15 +724,18 @@ def stress(
693724
concurrency: str,
694725
base_url: str | None,
695726
max_tokens: int | None,
727+
num_prompts: int | None,
696728
) -> None:
697729
"""Stress-test a backend with varying concurrency levels."""
698730
from infer_check.backends.base import get_backend_for_model
699731
from infer_check.runner import TestRunner
700732
from infer_check.suites.loader import load_suite
701733

702-
# Update max_tokens from subcommand if provided
734+
# Update defaults from subcommand if provided
703735
if max_tokens is not None:
704736
ctx.obj["max_tokens"] = max_tokens
737+
if num_prompts is not None:
738+
ctx.obj["num_prompts"] = num_prompts
705739

706740
concurrency_levels = [int(c.strip()) for c in concurrency.split(",") if c.strip()]
707741

@@ -716,6 +750,9 @@ def stress(
716750
)
717751

718752
prompt_list = load_suite(_resolve_prompts(prompts))
753+
if ctx.obj["num_prompts"] is not None:
754+
prompt_list = prompt_list[: ctx.obj["num_prompts"]]
755+
719756
# Apply global max_tokens only if not explicitly set in the prompt JSONL
720757
for p in prompt_list:
721758
if "max_tokens" not in p.model_fields_set:
@@ -790,15 +827,18 @@ def determinism(
790827
runs: int,
791828
base_url: str | None,
792829
max_tokens: int | None,
830+
num_prompts: int | None,
793831
) -> None:
794832
"""Test whether a backend produces identical outputs across repeated runs at temperature=0."""
795833
from infer_check.backends.base import get_backend_for_model
796834
from infer_check.runner import TestRunner
797835
from infer_check.suites.loader import load_suite
798836

799-
# Update max_tokens from subcommand if provided
837+
# Update defaults from subcommand if provided
800838
if max_tokens is not None:
801839
ctx.obj["max_tokens"] = max_tokens
840+
if num_prompts is not None:
841+
ctx.obj["num_prompts"] = num_prompts
802842

803843
backend_instance = get_backend_for_model(
804844
model_str=model,
@@ -809,6 +849,9 @@ def determinism(
809849
console.print(f"[bold cyan]determinism[/bold cyan] model={model} backend={backend_instance.name} runs={runs}")
810850

811851
prompt_list = load_suite(_resolve_prompts(prompts))
852+
if ctx.obj["num_prompts"] is not None:
853+
prompt_list = prompt_list[: ctx.obj["num_prompts"]]
854+
812855
# Apply global max_tokens only if not explicitly set in the prompt JSONL
813856
for p in prompt_list:
814857
if "max_tokens" not in p.model_fields_set:

tests/unit/test_cli_num_prompts.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import json
2+
from datetime import UTC, datetime
3+
from pathlib import Path
4+
from unittest.mock import patch
5+
6+
from click.testing import CliRunner
7+
8+
from infer_check.cli import main
9+
from infer_check.runner import TestRunner
10+
from infer_check.types import SweepResult
11+
12+
13+
def test_num_prompts_limit() -> None:
14+
"""Test that --num-prompts correctly limits the number of prompts used."""
15+
runner = CliRunner()
16+
with runner.isolated_filesystem():
17+
prompt_path = Path("test_prompts.jsonl")
18+
prompts = [
19+
{"text": "Prompt 1"},
20+
{"text": "Prompt 2"},
21+
{"text": "Prompt 3"},
22+
{"text": "Prompt 4"},
23+
]
24+
prompt_path.write_text("\n".join(json.dumps(p) for p in prompts), encoding="utf-8")
25+
26+
mock_result = SweepResult(
27+
model_id="m1",
28+
backend_name="mlx",
29+
quantization_levels=["a", "b"],
30+
comparisons=[],
31+
timestamp=datetime.now(UTC),
32+
summary={},
33+
)
34+
35+
with (
36+
patch("infer_check.cli._resolve_prompts", return_value=prompt_path),
37+
patch("infer_check.backends.base.get_backend_for_model"),
38+
patch.object(TestRunner, "sweep", return_value=mock_result) as mock_sweep,
39+
):
40+
# 1. Test global --num-prompts
41+
result = runner.invoke(
42+
main, ["--num-prompts", "2", "sweep", "--models", "a=m1,b=m2", "--prompts", "test_prompts.jsonl"]
43+
)
44+
assert result.exit_code == 0
45+
args, kwargs = mock_sweep.call_args
46+
captured_prompts = kwargs.get("prompts") or args[2]
47+
assert len(captured_prompts) == 2
48+
assert captured_prompts[0].text == "Prompt 1"
49+
assert captured_prompts[1].text == "Prompt 2"
50+
51+
# 2. Test subcommand --num-prompts override
52+
result = runner.invoke(
53+
main,
54+
[
55+
"--num-prompts",
56+
"2",
57+
"sweep",
58+
"--models",
59+
"a=m1,b=m2",
60+
"--prompts",
61+
"test_prompts.jsonl",
62+
"--num-prompts",
63+
"3",
64+
],
65+
)
66+
assert result.exit_code == 0
67+
args, kwargs = mock_sweep.call_args
68+
captured_prompts = kwargs.get("prompts") or args[2]
69+
assert len(captured_prompts) == 3
70+
71+
# 3. Test without --num-prompts (should use all)
72+
result = runner.invoke(main, ["sweep", "--models", "a=m1,b=m2", "--prompts", "test_prompts.jsonl"])
73+
assert result.exit_code == 0
74+
args, kwargs = mock_sweep.call_args
75+
captured_prompts = kwargs.get("prompts") or args[2]
76+
assert len(captured_prompts) == 4

0 commit comments

Comments
 (0)