diff --git a/README.md b/README.md index 76f3d1c..bf5da61 100644 --- a/README.md +++ b/README.md @@ -198,6 +198,12 @@ Complete provenance and linked-graph validation are stored in: `reports/qualcomm_qnn_v0_1.json` +The exact linked multi-graph QNN context binary is tracked at +`artifacts/qualcomm_ai_hub/current_model/edgegenbench_multigraph.bin`. The +portfolio acceptance builder verifies its committed Git-blob SHA-256 and size +against the Qualcomm report, so CI fails closed if the deployment artifact is +missing or altered. + The authenticated current-model run is reproducible with: ```bash diff --git a/artifacts/qualcomm_ai_hub/current_model/edgegenbench_multigraph.bin b/artifacts/qualcomm_ai_hub/current_model/edgegenbench_multigraph.bin new file mode 100644 index 0000000..00c0e00 Binary files /dev/null and b/artifacts/qualcomm_ai_hub/current_model/edgegenbench_multigraph.bin differ diff --git a/docs/qualcomm_ai_hub_qnn.md b/docs/qualcomm_ai_hub_qnn.md index 890a7f3..88a4455 100644 --- a/docs/qualcomm_ai_hub_qnn.md +++ b/docs/qualcomm_ai_hub_qnn.md @@ -209,6 +209,13 @@ The complete machine-readable result is stored in: `reports/qualcomm_qnn_v0_1.json` +The linked QNN context binary used by that report is stored in: + +`artifacts/qualcomm_ai_hub/current_model/edgegenbench_multigraph.bin` + +`scripts/build_portfolio_acceptance.py` validates the committed binary's size +and SHA-256 before accepting the Qualcomm lane. + ## Credential boundary The Qualcomm AI Hub token is configured outside the repository. diff --git a/reports/portfolio_acceptance.json b/reports/portfolio_acceptance.json index 492b718..405878f 100644 --- a/reports/portfolio_acceptance.json +++ b/reports/portfolio_acceptance.json @@ -18,6 +18,11 @@ "backend": "QNN HTP", "qairt_version": "2.45.0.260326154327", "context_model_id": "mnjjex79n", + "context_path": "artifacts/qualcomm_ai_hub/current_model/edgegenbench_multigraph.bin", + "context_sha256": "43d7cb889b0dd97d8de3a48557fdc7dceb322e6c7b72fdb91b19e5473f84b0df", + "context_size_bytes": 106496, + "context_hash_origin": "committed_git_blob", + "context_matches_repository": true, "link_job_id": "jgk4d8lvp", "graphs": [ { diff --git a/reports/portfolio_acceptance.md b/reports/portfolio_acceptance.md index 5ef849c..9b24ca5 100644 --- a/reports/portfolio_acceptance.md +++ b/reports/portfolio_acceptance.md @@ -13,6 +13,7 @@ Device: **Snapdragon 8 Elite QRD**; backend: **QNN HTP**; QAIRT: `2.45.0.260326154327`. Source-model provenance match: **True**. +Tracked QNN context provenance match: **True** (`artifacts/qualcomm_ai_hub/current_model/edgegenbench_multigraph.bin`, `43d7cb889b0dd97d8de3a48557fdc7dceb322e6c7b72fdb91b19e5473f84b0df`). | Batch | AI Hub latency (ms) | Throughput (samples/s) | Peak memory (bytes) | Placement | Max normalized drift | |---:|---:|---:|---:|---|---:| diff --git a/reports/qualcomm_qnn_v0_1.json b/reports/qualcomm_qnn_v0_1.json index 860e1a9..4e8a49a 100644 --- a/reports/qualcomm_qnn_v0_1.json +++ b/reports/qualcomm_qnn_v0_1.json @@ -44,6 +44,7 @@ "link_status": "JobStatus\n---------\ncode : SUCCESS\nmessage : \n", "target_model_id": "mnjjex79n", "target_model_type": "SourceModelType.QNN_CONTEXT_BINARY", + "serialized_model_path": "artifacts/qualcomm_ai_hub/current_model/edgegenbench_multigraph.bin", "serialized_model_size_bytes": 106496, "serialized_model_sha256": "43d7cb889b0dd97d8de3a48557fdc7dceb322e6c7b72fdb91b19e5473f84b0df", "target_metadata": { diff --git a/scripts/build_portfolio_acceptance.py b/scripts/build_portfolio_acceptance.py index 223b255..68a460b 100644 --- a/scripts/build_portfolio_acceptance.py +++ b/scripts/build_portfolio_acceptance.py @@ -65,6 +65,14 @@ def validate_ai_hub_qnn(report_path: Path, repository_root: Path) -> dict[str, A raise ValueError("Qualcomm report must identify the HTP backend and QAIRT version") if "SUCCESS" not in str(linked.get("link_status")): raise ValueError("linked QNN context job did not succeed") + context_path = repository_root / str(linked.get("serialized_model_path")) + context_sha256, context_hash_origin = _repository_sha256(repository_root, context_path) + context_size = context_path.stat().st_size if context_path.is_file() else None + context_matches = context_sha256 == linked.get( + "serialized_model_sha256" + ) and context_size == linked.get("serialized_model_size_bytes") + if not context_matches: + raise ValueError("tracked QNN context binary does not match its reported hash and size") validation = linked.get("validation") if not isinstance(validation, dict) or validation.get("device") != hardware.get("device"): raise ValueError("linked QNN validation device does not match the report") @@ -111,6 +119,11 @@ def validate_ai_hub_qnn(report_path: Path, repository_root: Path) -> dict[str, A "backend": "QNN HTP", "qairt_version": hardware["qairt_version"], "context_model_id": linked["target_model_id"], + "context_path": linked["serialized_model_path"], + "context_sha256": context_sha256, + "context_size_bytes": context_size, + "context_hash_origin": context_hash_origin, + "context_matches_repository": context_matches, "link_job_id": linked["link_job_id"], "graphs": sorted(graph_summaries, key=lambda value: value["batch_size"]), "reported_source_model_sha256": source.get("sha256"), @@ -176,6 +189,8 @@ def build_portfolio_acceptance( f"Device: **{qnn['device']}**; backend: **{qnn['backend']}**; " f"QAIRT: `{qnn['qairt_version']}`.", f"Source-model provenance match: **{qnn['source_model_matches_repository']}**.", + f"Tracked QNN context provenance match: **{qnn['context_matches_repository']}** " + f"(`{qnn['context_path']}`, `{qnn['context_sha256']}`).", "", "| Batch | AI Hub latency (ms) | Throughput (samples/s) | " "Peak memory (bytes) | Placement | Max normalized drift |", diff --git a/scripts/rerun_qualcomm_qnn_current_model.py b/scripts/rerun_qualcomm_qnn_current_model.py index 72947d0..8177cf7 100644 --- a/scripts/rerun_qualcomm_qnn_current_model.py +++ b/scripts/rerun_qualcomm_qnn_current_model.py @@ -319,6 +319,7 @@ def main() -> None: "link_status": str(link_job.get_status()), "target_model_id": target_model.model_id, "target_model_type": str(target_model.model_type), + "serialized_model_path": str(CONTEXT_PATH), "serialized_model_size_bytes": CONTEXT_PATH.stat().st_size, "serialized_model_sha256": _sha256(CONTEXT_PATH), "target_metadata": metadata, diff --git a/tests/test_portfolio_acceptance.py b/tests/test_portfolio_acceptance.py index d0e73a6..f70cc66 100644 --- a/tests/test_portfolio_acceptance.py +++ b/tests/test_portfolio_acceptance.py @@ -19,6 +19,8 @@ def test_validates_tracked_ai_hub_qnn_evidence() -> None: assert result["backend"] == "QNN HTP" assert result["source_model_matches_repository"] is True assert result["source_model_hash_origin"] == "committed_git_blob" + assert result["context_matches_repository"] is True + assert result["context_hash_origin"] == "committed_git_blob" assert len(cast(list[object], result["graphs"])) == 3 @@ -32,3 +34,13 @@ def test_rejects_cpu_compute_unit_in_qnn_report(tmp_path: Path) -> None: path.write_text(json.dumps(report)) with pytest.raises(ValueError, match="exclusive NPU"): validate_ai_hub_qnn(path, root) + + +def test_rejects_mismatched_qnn_context_hash(tmp_path: Path) -> None: + root = Path(__file__).parents[1] + report = json.loads((root / "reports/qualcomm_qnn_v0_1.json").read_text()) + report["linked_multigraph"]["serialized_model_sha256"] = "0" * 64 + path = tmp_path / "qnn.json" + path.write_text(json.dumps(report)) + with pytest.raises(ValueError, match="context binary"): + validate_ai_hub_qnn(path, root)