chipwright 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- chipwright/__init__.py +11 -0
- chipwright/adapt.py +57 -0
- chipwright/build.py +329 -0
- chipwright/cache.py +65 -0
- chipwright/cli.py +242 -0
- chipwright/config.py +106 -0
- chipwright/data/behavior_prompts.yaml +108 -0
- chipwright/data/index.yaml +327 -0
- chipwright/device.py +143 -0
- chipwright/device_adb.py +121 -0
- chipwright/env.py +229 -0
- chipwright/errors.py +41 -0
- chipwright/llm_reference.py +114 -0
- chipwright/modalities/__init__.py +3 -0
- chipwright/modalities/asr_encoder.py +90 -0
- chipwright/modalities/cv.py +34 -0
- chipwright/modalities/llm.py +300 -0
- chipwright/recipe.py +205 -0
- chipwright/registry.py +65 -0
- chipwright/resolver.py +197 -0
- chipwright/run.py +173 -0
- chipwright/sdk.py +140 -0
- chipwright/tags.py +56 -0
- chipwright/task_eval.py +190 -0
- chipwright/tests/conftest.py +12 -0
- chipwright/tests/test_adapt.py +61 -0
- chipwright/tests/test_build.py +279 -0
- chipwright/tests/test_cache.py +147 -0
- chipwright/tests/test_device_adb.py +140 -0
- chipwright/tests/test_env.py +73 -0
- chipwright/tests/test_env_adb.py +120 -0
- chipwright/tests/test_llm_modality.py +259 -0
- chipwright/tests/test_packaging.py +147 -0
- chipwright/tests/test_precision_support.py +128 -0
- chipwright/tests/test_registry.py +107 -0
- chipwright/tests/test_resolver.py +93 -0
- chipwright/tests/test_run_llm.py +47 -0
- chipwright/tests/test_tags.py +95 -0
- chipwright/tests/test_task_eval.py +164 -0
- chipwright/tests/test_verify.py +92 -0
- chipwright/tests/test_verify_llm.py +244 -0
- chipwright/verify.py +24 -0
- chipwright/verify_llm.py +203 -0
- chipwright-0.2.0.dist-info/METADATA +268 -0
- chipwright-0.2.0.dist-info/RECORD +49 -0
- chipwright-0.2.0.dist-info/WHEEL +5 -0
- chipwright-0.2.0.dist-info/entry_points.txt +2 -0
- chipwright-0.2.0.dist-info/licenses/LICENSE +21 -0
- chipwright-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""The resolver is a pure function — the four outcomes are testable without a board."""
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
6
|
+
|
|
7
|
+
from chipwright.env import TargetKey
|
|
8
|
+
from chipwright.registry import Registry
|
|
9
|
+
from chipwright.resolver import Outcome, Variant, resolve
|
|
10
|
+
from chipwright.tags import ArtifactTag
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_tag_roundtrip():
|
|
14
|
+
s = "whisper-small-en-1.2.0+qnn2.37-htpv68-w8a16-win30s.qnnctx"
|
|
15
|
+
t = ArtifactTag.parse(s)
|
|
16
|
+
assert (t.name, t.version, t.sdk, t.arch, t.quant, t.shape, t.ext) == \
|
|
17
|
+
("whisper-small-en", "1.2.0", "2.37", "v68", "w8a16", "win30s", "qnnctx")
|
|
18
|
+
assert str(t) == s
|
|
19
|
+
assert t.is_context_binary
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_tag_rejects_garbage():
|
|
23
|
+
for bad in ["whisper.qnnctx", "x-1.0+htpv68.qnnctx", "x-1.0.0+qnn2.37-htpv68-w8a16.qnnctx"]:
|
|
24
|
+
try:
|
|
25
|
+
ArtifactTag.parse(bad); assert False, bad
|
|
26
|
+
except ValueError:
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _v(**kw):
|
|
31
|
+
base = dict(model="m", version="1.0.0", arch="v68", quant="w8a16", shape="win30s",
|
|
32
|
+
sdk_tested="2.37.1", sdk_compatible=">=2.34,<2.40")
|
|
33
|
+
base.update(kw)
|
|
34
|
+
return Variant(**base)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_use_exact():
|
|
38
|
+
t = TargetKey(htp_arch="v68", qairt="2.37.1")
|
|
39
|
+
d = resolve(t, [_v(url="u", sha256="s")])
|
|
40
|
+
assert d.outcome is Outcome.USE and d.ok
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_use_with_warn_sdk_in_range():
|
|
44
|
+
t = TargetKey(htp_arch="v68", qairt="2.38.0") # in range, not the tested point
|
|
45
|
+
d = resolve(t, [_v(url="u", sha256="s")])
|
|
46
|
+
assert d.outcome is Outcome.USE_WITH_WARN and d.ok and d.warning
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_offline_unknown_sdk_warns():
|
|
50
|
+
t = TargetKey(htp_arch="v68", qairt=None)
|
|
51
|
+
d = resolve(t, [_v(url="u", sha256="s")])
|
|
52
|
+
assert d.outcome is Outcome.USE_WITH_WARN
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_build_recipe_only():
|
|
56
|
+
t = TargetKey(htp_arch="v68", qairt="2.37.1")
|
|
57
|
+
d = resolve(t, [_v(recipe="r.yaml")]) # no url
|
|
58
|
+
assert d.outcome is Outcome.BUILD and not d.ok
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_fail_arch_names_axis():
|
|
62
|
+
t = TargetKey(htp_arch="v68", qairt="2.37.1")
|
|
63
|
+
d = resolve(t, [_v(arch="v73", url="u", sha256="s")])
|
|
64
|
+
assert d.outcome is Outcome.FAIL and d.axis == "htp_arch"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_fail_sdk_out_of_range_no_recipe():
|
|
68
|
+
t = TargetKey(htp_arch="v68", qairt="2.50.0") # outside the range, artifact but no recipe
|
|
69
|
+
d = resolve(t, [_v(url="u", sha256="s")])
|
|
70
|
+
assert d.outcome is Outcome.FAIL and d.axis == "qairt"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_sdk_out_of_range_but_recipe_builds():
|
|
74
|
+
t = TargetKey(htp_arch="v68", qairt="2.50.0")
|
|
75
|
+
d = resolve(t, [_v(url="u", sha256="s"), _v(recipe="r.yaml")])
|
|
76
|
+
assert d.outcome is Outcome.BUILD
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_seed_registry_loads():
|
|
80
|
+
reg = Registry()
|
|
81
|
+
assert "laya-decision" in reg.models() and "whisper-small-en" in reg.models()
|
|
82
|
+
v68 = TargetKey(htp_arch="v68", qairt="2.37.1")
|
|
83
|
+
assert resolve(v68, reg.variants("laya-decision")).ok # USE (exact or SDK-warn)
|
|
84
|
+
assert resolve(v68, reg.variants("whisper-small-en")).outcome is Outcome.BUILD
|
|
85
|
+
assert resolve(v68, reg.variants("qwen2.5-1.5b-instruct")).outcome is Outcome.BUILD # v68 variant exists
|
|
86
|
+
assert resolve(v68, reg.variants("qwen2.5-7b-instruct")).axis == "htp_arch" # v73/v75 only
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__":
|
|
90
|
+
fns = [f for n, f in sorted(globals().items()) if n.startswith("test_")]
|
|
91
|
+
for f in fns:
|
|
92
|
+
f(); print("ok", f.__name__)
|
|
93
|
+
print(f"\n{len(fns)} passed")
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""The LLM run path refuses by name when it can't gate — integration guards for run_llm_variant.
|
|
2
|
+
|
|
3
|
+
Mirrors the CV/ASR refusal-path tests: a pass is never reported without the gate, and the LLM path
|
|
4
|
+
refuses with a message naming the fix (no Genie bundle / no pinned reference), not a raw traceback.
|
|
5
|
+
No board needed — these never reach adb.
|
|
6
|
+
"""
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
13
|
+
|
|
14
|
+
from chipwright import run as runmod
|
|
15
|
+
from chipwright.resolver import Variant
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _llm_variant(**kw):
|
|
19
|
+
base = dict(model="qwen-x", version="1", arch="v81", quant="w4a16", shape="ctx4096",
|
|
20
|
+
sdk_tested="2.51", sdk_compatible=">=2.34,<2.60")
|
|
21
|
+
base.update(kw)
|
|
22
|
+
return Variant(**base)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_llm_run_refuses_when_no_bundle():
|
|
26
|
+
"""An LLM variant with no Genie bundle directory refuses by name (build / CW_REGISTRY)."""
|
|
27
|
+
v = _llm_variant(run={"modality": "llm"}) # no local bundle dir
|
|
28
|
+
with pytest.raises(RuntimeError, match="bundle|chipwright build|CW_REGISTRY"):
|
|
29
|
+
runmod.run_variant(v)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_llm_run_refuses_when_no_pinned_reference(tmp_path):
|
|
33
|
+
"""A bundle present but no pinned reference → refuse: the generative gate can't run."""
|
|
34
|
+
bundle = tmp_path / "bundle"
|
|
35
|
+
bundle.mkdir()
|
|
36
|
+
(bundle / "genie_config.json").write_text("{}")
|
|
37
|
+
v = _llm_variant(local=str(bundle), run={"modality": "llm"}) # bundle, but no `reference`
|
|
38
|
+
with pytest.raises(RuntimeError, match="pinned reference|gate can't run"):
|
|
39
|
+
runmod.run_variant(v)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_looks_refused_classifier():
|
|
43
|
+
"""The v1 behavioral classifier tells a refusal from a compliant answer."""
|
|
44
|
+
assert runmod._looks_refused("I can't help with that.") is True
|
|
45
|
+
assert runmod._looks_refused("I'm sorry, but I cannot assist.") is True
|
|
46
|
+
assert runmod._looks_refused("The capital of France is Paris.") is False
|
|
47
|
+
assert runmod._looks_refused("") is False
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""ArtifactTag — the wheel-tag grammar. Strict parse, round-trip, context-binary classification.
|
|
2
|
+
|
|
3
|
+
Pure string logic, no board. Also runnable as `python3 test_tags.py`.
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
9
|
+
|
|
10
|
+
from chipwright.tags import ArtifactTag
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_parse_roundtrips_valid_tag():
|
|
14
|
+
s = "whisper-small-en-1.2.0+qnn2.37-htpv68-w8a16-win30s.qnnctx"
|
|
15
|
+
t = ArtifactTag.parse(s)
|
|
16
|
+
assert t.name == "whisper-small-en"
|
|
17
|
+
assert t.version == "1.2.0"
|
|
18
|
+
assert t.sdk == "2.37"
|
|
19
|
+
assert t.arch == "v68"
|
|
20
|
+
assert t.quant == "w8a16"
|
|
21
|
+
assert t.shape == "win30s"
|
|
22
|
+
assert t.ext == "qnnctx"
|
|
23
|
+
# str() is the exact inverse of parse()
|
|
24
|
+
assert str(t) == s
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_parse_handles_hyphenated_name_and_fp_quant():
|
|
28
|
+
# name may contain hyphens (greedy up to the last -<semver>+); quant may be fp16/fp32.
|
|
29
|
+
s = "laya-decision-tiny-10.20.30+qnn2.34-htpv73-fp16-ctx4096.dlc"
|
|
30
|
+
t = ArtifactTag.parse(s)
|
|
31
|
+
assert t.name == "laya-decision-tiny"
|
|
32
|
+
assert t.version == "10.20.30"
|
|
33
|
+
assert t.arch == "v73"
|
|
34
|
+
assert t.quant == "fp16"
|
|
35
|
+
assert t.ext == "dlc"
|
|
36
|
+
assert str(t) == s
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_parse_strips_surrounding_whitespace():
|
|
40
|
+
s = "m-1.0.0+qnn2.37-htpv68-w8a16-win30s.bin"
|
|
41
|
+
t = ArtifactTag.parse(" " + s + "\n")
|
|
42
|
+
assert str(t) == s
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_rejects_missing_version():
|
|
46
|
+
for bad in [
|
|
47
|
+
"whisper.qnnctx", # no version at all
|
|
48
|
+
"whisper-small+qnn2.37-htpv68-w8a16-win30s.qnnctx", # name but no semver
|
|
49
|
+
"whisper-1.0+qnn2.37-htpv68-w8a16-win30s.qnnctx", # two-part version, not semver
|
|
50
|
+
]:
|
|
51
|
+
_assert_rejected(bad)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_rejects_missing_arch():
|
|
55
|
+
for bad in [
|
|
56
|
+
"whisper-1.0.0+qnn2.37-w8a16-win30s.qnnctx", # no htp arch segment
|
|
57
|
+
"whisper-1.0.0+qnn2.37-htp68-w8a16-win30s.qnnctx", # arch missing the 'v'
|
|
58
|
+
"whisper-1.0.0+htpv68-w8a16-win30s.qnnctx", # no +qnn sdk tag
|
|
59
|
+
]:
|
|
60
|
+
_assert_rejected(bad)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_rejects_bad_extension():
|
|
64
|
+
for bad in [
|
|
65
|
+
"whisper-1.0.0+qnn2.37-htpv68-w8a16-win30s.onnx", # not an allowed ext
|
|
66
|
+
"whisper-1.0.0+qnn2.37-htpv68-w8a16-win30s", # no ext
|
|
67
|
+
"whisper-1.0.0+qnn2.37-htpv68-w8a16-win30s.qnnctx.bak",
|
|
68
|
+
]:
|
|
69
|
+
_assert_rejected(bad)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_is_context_binary_true_for_qnnctx_and_bin():
|
|
73
|
+
for ext in ("qnnctx", "bin"):
|
|
74
|
+
s = f"m-1.0.0+qnn2.37-htpv68-w8a16-win30s.{ext}"
|
|
75
|
+
assert ArtifactTag.parse(s).is_context_binary, ext
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_is_context_binary_false_for_dlc():
|
|
79
|
+
s = "m-1.0.0+qnn2.37-htpv68-w8a16-win30s.dlc"
|
|
80
|
+
assert ArtifactTag.parse(s).is_context_binary is False
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _assert_rejected(bad: str):
|
|
84
|
+
try:
|
|
85
|
+
ArtifactTag.parse(bad)
|
|
86
|
+
except ValueError:
|
|
87
|
+
return
|
|
88
|
+
raise AssertionError(f"expected ValueError for {bad!r}")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
if __name__ == "__main__":
|
|
92
|
+
fns = [f for n, f in sorted(globals().items()) if n.startswith("test_")]
|
|
93
|
+
for f in fns:
|
|
94
|
+
f(); print("ok", f.__name__)
|
|
95
|
+
print(f"\n{len(fns)} passed")
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Layer-2 task eval + behavioral-probe data — record-assembly, the lazy-import guard, the prompt sets.
|
|
2
|
+
|
|
3
|
+
Pure / no heavy deps: the record-assembly is exercised with a FAKE scorer (no qai-hub, no model, no
|
|
4
|
+
dataset), and the lazy-import-missing path is forced deterministically regardless of whether
|
|
5
|
+
`qai-hub-models` happens to be installed. Runnable as `python3 test_task_eval.py`.
|
|
6
|
+
"""
|
|
7
|
+
import builtins
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
import yaml
|
|
13
|
+
|
|
14
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
15
|
+
|
|
16
|
+
import chipwright
|
|
17
|
+
from chipwright import task_eval
|
|
18
|
+
from chipwright.task_eval import (
|
|
19
|
+
TaskSpec, TASKS, resolve_task, task_record, run_task_eval, _qai_hub_scorer, _require_qai_hub,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# --- pure record-assembly (task_record) -----------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
def test_task_record_shape_and_types():
|
|
26
|
+
rec = task_record("wikitext_perplexity", 10.58, "wikitext", "test", 256)
|
|
27
|
+
assert rec == {
|
|
28
|
+
"metric": "wikitext_perplexity", "value": 10.58, "dataset": "wikitext",
|
|
29
|
+
"split": "test", "samples": 256,
|
|
30
|
+
}
|
|
31
|
+
assert isinstance(rec["value"], float) and isinstance(rec["samples"], int)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_task_record_coerces_value_and_samples():
|
|
35
|
+
rec = task_record("mmlu_accuracy", 1, "mmlu", "test", "256") # int value, str samples
|
|
36
|
+
assert rec["value"] == 1.0 and isinstance(rec["value"], float)
|
|
37
|
+
assert rec["samples"] == 256 and isinstance(rec["samples"], int)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# --- resolve_task ---------------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
def test_resolve_task_known_names():
|
|
43
|
+
assert resolve_task("wikitext").metric == "wikitext_perplexity"
|
|
44
|
+
assert resolve_task("mmlu").dataset == "mmlu"
|
|
45
|
+
assert resolve_task("tiny_mmlu").evaluator == "mmlu"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_resolve_task_passthrough_spec():
|
|
49
|
+
spec = TaskSpec("x", "m", "d", "perplexity")
|
|
50
|
+
assert resolve_task(spec) is spec
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_resolve_task_unknown_refuses_by_name():
|
|
54
|
+
with pytest.raises(ValueError, match="unknown task"):
|
|
55
|
+
resolve_task("not_a_dataset")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# --- run_task_eval with an injected FAKE scorer (no heavy dep, no model, no dataset) ---------------
|
|
59
|
+
|
|
60
|
+
def test_run_task_eval_with_fake_scorer_assembles_records():
|
|
61
|
+
calls = []
|
|
62
|
+
|
|
63
|
+
def fake_scorer(spec, split, samples):
|
|
64
|
+
calls.append((resolve_task(spec).name, split, samples))
|
|
65
|
+
return {"wikitext_perplexity": 10.58, "mmlu_accuracy": 0.554}[resolve_task(spec).metric]
|
|
66
|
+
|
|
67
|
+
out = run_task_eval(tasks=("wikitext", "mmlu"), samples=256, split="test", scorer=fake_scorer)
|
|
68
|
+
assert out == [
|
|
69
|
+
{"metric": "wikitext_perplexity", "value": 10.58, "dataset": "wikitext",
|
|
70
|
+
"split": "test", "samples": 256},
|
|
71
|
+
{"metric": "mmlu_accuracy", "value": 0.554, "dataset": "mmlu",
|
|
72
|
+
"split": "test", "samples": 256},
|
|
73
|
+
]
|
|
74
|
+
# the scorer saw each task with the requested split/samples (provenance threads through)
|
|
75
|
+
assert calls == [("wikitext", "test", 256), ("mmlu", "test", 256)]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_run_task_eval_fake_scorer_needs_no_generator_or_dep():
|
|
79
|
+
# the injectable seam is exactly what makes this testable with no qai-hub and no model present
|
|
80
|
+
out = run_task_eval(tasks=("tiny_mmlu",), samples=100, scorer=lambda s, sp, n: 0.61)
|
|
81
|
+
assert out == [{"metric": "mmlu_accuracy", "value": 0.61, "dataset": "tiny_mmlu",
|
|
82
|
+
"split": "test", "samples": 100}]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# --- the lazy-import guard: raises BY NAME when qai-hub-models is absent ---------------------------
|
|
86
|
+
|
|
87
|
+
def test_require_qai_hub_raises_by_name_when_absent(monkeypatch):
|
|
88
|
+
# force the import to fail regardless of whether qai-hub-models is installed in this env
|
|
89
|
+
real_import = builtins.__import__
|
|
90
|
+
|
|
91
|
+
def fake_import(name, *a, **k):
|
|
92
|
+
if name.startswith("qai_hub_models"):
|
|
93
|
+
raise ImportError("No module named 'qai_hub_models'")
|
|
94
|
+
return real_import(name, *a, **k)
|
|
95
|
+
|
|
96
|
+
monkeypatch.setattr(builtins, "__import__", fake_import)
|
|
97
|
+
with pytest.raises(RuntimeError, match=r"qai-hub-models|chipwright\[eval\]"):
|
|
98
|
+
_require_qai_hub()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_qai_hub_scorer_refuses_missing_generator_by_name(monkeypatch):
|
|
102
|
+
# with the dep "present" (stubbed), a missing generator is still refused by name before any download
|
|
103
|
+
monkeypatch.setattr(task_eval, "_require_qai_hub", lambda: {})
|
|
104
|
+
with pytest.raises(RuntimeError, match="generator"):
|
|
105
|
+
_qai_hub_scorer(None)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_qai_hub_scorer_refuses_missing_tokenizer_by_name(monkeypatch):
|
|
109
|
+
monkeypatch.setattr(task_eval, "_require_qai_hub", lambda: {})
|
|
110
|
+
with pytest.raises(RuntimeError, match="tokenizer"):
|
|
111
|
+
_qai_hub_scorer(object(), tokenizer=None)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_importing_task_eval_does_not_import_qai_hub():
|
|
115
|
+
# the heavy dep must NOT be pulled in at module import (preflight discipline)
|
|
116
|
+
assert "qai_hub_models" not in sys.modules
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# --- the behavioral-probe data file ---------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
def _load_behavior_prompts():
|
|
122
|
+
path = os.path.join(os.path.dirname(chipwright.__file__), "data", "behavior_prompts.yaml")
|
|
123
|
+
with open(path) as f:
|
|
124
|
+
return yaml.safe_load(f)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_behavior_prompts_loads_with_both_sets_nonempty():
|
|
128
|
+
data = _load_behavior_prompts()
|
|
129
|
+
assert set(data) >= {"harmful", "benign"}
|
|
130
|
+
assert isinstance(data["harmful"], list) and len(data["harmful"]) > 0
|
|
131
|
+
assert isinstance(data["benign"], list) and len(data["benign"]) > 0
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_behavior_prompts_are_surface_twinned_and_well_formed():
|
|
135
|
+
data = _load_behavior_prompts()
|
|
136
|
+
# aligned by index (every harmful item has a benign control at the same position)
|
|
137
|
+
assert len(data["harmful"]) == len(data["benign"]) == 24
|
|
138
|
+
for p in data["harmful"] + data["benign"]:
|
|
139
|
+
assert isinstance(p, str) and p.strip() # non-empty strings, no stray nulls
|
|
140
|
+
# the two sets are disjoint — a benign twin must not accidentally repeat a harmful prompt
|
|
141
|
+
assert set(data["harmful"]).isdisjoint(data["benign"])
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
if __name__ == "__main__":
|
|
145
|
+
fns = [f for n, f in sorted(globals().items()) if n.startswith("test_") and callable(f)]
|
|
146
|
+
import types
|
|
147
|
+
|
|
148
|
+
class _MP: # minimal monkeypatch shim for the __main__ runner
|
|
149
|
+
def __init__(self): self._undo = []
|
|
150
|
+
def setattr(self, obj, name, val):
|
|
151
|
+
old = getattr(obj, name); self._undo.append((obj, name, old)); setattr(obj, name, val)
|
|
152
|
+
def undo(self):
|
|
153
|
+
for obj, name, old in reversed(self._undo): setattr(obj, name, old)
|
|
154
|
+
|
|
155
|
+
passed = 0
|
|
156
|
+
for f in fns:
|
|
157
|
+
mp = _MP()
|
|
158
|
+
try:
|
|
159
|
+
n = f.__code__.co_argcount
|
|
160
|
+
f(mp) if n == 1 else f()
|
|
161
|
+
print("ok", f.__name__); passed += 1
|
|
162
|
+
finally:
|
|
163
|
+
mp.undo()
|
|
164
|
+
print(f"\n{passed} passed")
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Numerical verification — cosine similarity and the per-output pass/fail report.
|
|
2
|
+
|
|
3
|
+
Pure numpy, no board. Runnable as `python3 test_verify.py`.
|
|
4
|
+
"""
|
|
5
|
+
import math
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
12
|
+
|
|
13
|
+
from chipwright.verify import cosine, report
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_cosine_of_identical_vectors_is_one():
|
|
17
|
+
a = np.array([1.0, 2.0, 3.0, 4.0])
|
|
18
|
+
assert cosine(a, a) == 1.0
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_cosine_of_orthogonal_vectors_is_zero():
|
|
22
|
+
a = np.array([1.0, 0.0])
|
|
23
|
+
b = np.array([0.0, 1.0])
|
|
24
|
+
assert abs(cosine(a, b)) < 1e-12
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_cosine_of_opposite_vectors_is_minus_one():
|
|
28
|
+
a = np.array([1.0, 2.0, 3.0])
|
|
29
|
+
assert math.isclose(cosine(a, -a), -1.0, rel_tol=1e-12)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_cosine_is_shape_agnostic_via_ravel():
|
|
33
|
+
# a 2-D NPU output and its flattened reference compare equal
|
|
34
|
+
a = np.arange(6, dtype=np.float64).reshape(2, 3)
|
|
35
|
+
b = np.arange(6, dtype=np.float64)
|
|
36
|
+
assert math.isclose(cosine(a, b), 1.0, rel_tol=1e-12)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_cosine_handles_zero_vector_without_crashing():
|
|
40
|
+
a = np.zeros(4)
|
|
41
|
+
b = np.array([1.0, 2.0, 3.0, 4.0])
|
|
42
|
+
assert cosine(a, b) == 0.0
|
|
43
|
+
assert cosine(a, a) == 0.0
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_report_passes_when_all_outputs_meet_threshold():
|
|
47
|
+
npu = {"logits": np.array([1.0, 2.0, 3.0]), "hidden": np.array([0.5, 0.5])}
|
|
48
|
+
cpu = {"logits": np.array([1.0, 2.0, 3.0]), "hidden": np.array([0.5, 0.5])}
|
|
49
|
+
r = report(npu, cpu, threshold=0.99)
|
|
50
|
+
assert r["pass"] is True
|
|
51
|
+
assert math.isclose(r["cosine_min"], 1.0, rel_tol=1e-12)
|
|
52
|
+
assert r["threshold"] == 0.99
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_report_fails_when_an_output_is_below_threshold():
|
|
56
|
+
npu = {"good": np.array([1.0, 1.0]), "bad": np.array([1.0, 0.0])}
|
|
57
|
+
cpu = {"good": np.array([1.0, 1.0]), "bad": np.array([0.0, 1.0])} # orthogonal -> cosine 0
|
|
58
|
+
r = report(npu, cpu, threshold=0.99)
|
|
59
|
+
assert r["pass"] is False
|
|
60
|
+
assert math.isclose(r["cosine_min"], 0.0, abs_tol=1e-12)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_report_per_output_dict_is_keyed_by_output_name():
|
|
64
|
+
npu = {"a": np.array([1.0, 0.0]), "b": np.array([0.0, 1.0])}
|
|
65
|
+
cpu = {"a": np.array([1.0, 0.0]), "b": np.array([0.0, 1.0])}
|
|
66
|
+
r = report(npu, cpu)
|
|
67
|
+
assert set(r["per_output"]) == {"a", "b"}
|
|
68
|
+
assert math.isclose(r["per_output"]["a"], 1.0, rel_tol=1e-12)
|
|
69
|
+
# cosine_min is the minimum over the per-output values
|
|
70
|
+
assert r["cosine_min"] == min(r["per_output"].values())
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_report_only_compares_shared_keys():
|
|
74
|
+
# keys present in npu but missing from cpu are skipped, not an error
|
|
75
|
+
npu = {"shared": np.array([1.0, 1.0]), "npu_only": np.array([1.0])}
|
|
76
|
+
cpu = {"shared": np.array([1.0, 1.0]), "cpu_only": np.array([1.0])}
|
|
77
|
+
r = report(npu, cpu)
|
|
78
|
+
assert set(r["per_output"]) == {"shared"}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_report_empty_inputs_do_not_crash():
|
|
82
|
+
r = report({}, {})
|
|
83
|
+
assert r["per_output"] == {}
|
|
84
|
+
assert r["cosine_min"] == 0.0
|
|
85
|
+
assert r["pass"] is False # 0.0 < default 0.99
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
if __name__ == "__main__":
|
|
89
|
+
fns = [f for n, f in sorted(globals().items()) if n.startswith("test_")]
|
|
90
|
+
for f in fns:
|
|
91
|
+
f(); print("ok", f.__name__)
|
|
92
|
+
print(f"\n{len(fns)} passed")
|