diff --git a/tests/assets/resnet.md b/tests/assets/resnet.md new file mode 100644 index 000000000..79484877a --- /dev/null +++ b/tests/assets/resnet.md @@ -0,0 +1,7 @@ +# resnet_w8a8 + +Generated by using hf-tiny-model-private/tiny-random-ResNetForImageClassification in FTK's AMD npu Microsoft/resnet-50 recipe. + +# resnet_fp32 + +The output model of the first part of the above process. diff --git a/tests/assets/resnet_fp32/model.onnx b/tests/assets/resnet_fp32/model.onnx new file mode 100644 index 000000000..a33b44f9d Binary files /dev/null and b/tests/assets/resnet_fp32/model.onnx differ diff --git a/tests/assets/resnet_fp32/model.onnx.data b/tests/assets/resnet_fp32/model.onnx.data new file mode 100644 index 000000000..68e97b030 Binary files /dev/null and b/tests/assets/resnet_fp32/model.onnx.data differ diff --git a/tests/assets/resnet_w8a8/model.onnx b/tests/assets/resnet_w8a8/model.onnx new file mode 100644 index 000000000..ffbd13c55 Binary files /dev/null and b/tests/assets/resnet_w8a8/model.onnx differ diff --git a/tests/assets/resnet_w8a8/model.onnx.data b/tests/assets/resnet_w8a8/model.onnx.data new file mode 100644 index 000000000..049f5063a --- /dev/null +++ b/tests/assets/resnet_w8a8/model.onnx.data @@ -0,0 +1 @@ +zyKrZHG\kAؤp[}fz4Rc^{KV`5}UWxw|sgRefUR;ythcdiif}fYkbybJpXqlolzmfk_\yx:P4lugy9c`wgmfJR0erxD^`UGs^EhZ|XjoԖzTGsxhsz{pv}dŧOnaaC]`q]WC*~Pxljwo]KxfqiI1LqNLag\shwwAZ_O>~mgasi?wbDtAxCHpqgo{W{ngjFM]x^[ygPo\.tzXrvsqwt:kf|qxckiu|U}rr?_buyzY9Iv~fyY^Z`TPw]dgWzjOwPsEZo\p]Zgwsp@hrdteF傁B=xq\]}{`jYL]\lT|leޠa||%o}>5r~VvP`s`PyatϑG[gR~kh\3RaC*|b|VYE^8Zr~~"ytoplpsOxO~~YsPpvmZP\I~<};pvxuIRtzposSpzrch_zlq!oovho9qhm;wblxMn7mifpD{|]p\iQalt}xï[uwohHxjR\^(\bw,}HwojjK}|Pqߞd-kVabEƠϛh}tulo`LQ{}Qf{}iaUms:nCShpG=va7nL{sdwq\sefJuh{vp}фw~KSphU@w~ts~lWn{~IuyLLfks/qPobG|st_~||uJ,T?=dHm°^nftݨexRxHTm>zwyu`c{thqt]\fTY@LTpO'e\Hpyjgnx_q|bħoddƢp|fd~PVj~yMTgV~{q{RwrXxzxPtpwAzg_sNs]уWMIfJmRj`i\fSnk~kzitxMzlprm_dOzqZu]7eurpW&~ȑmvoDelsqJxvy|rJ`u7hS]xny_mNexhSHX]FiMxikjl]7FNc|ClQmAryrnIxQVdJvXi@JBydĺπ{tyߏt^\9Ktw?DY:nMG“jv?ƁV_uWnd%deweۉanWKyQ_h@:~ٍ~ShuIZUAnP}WrRyTffbwU[/tԓhs beZmqhzy}p5s?}XG4^Jo~ykqtvrtqB^imvaT=jYdW_nv7XzaaҬkxsZ{v[vKH`hx]h~>bmMp*y_leuygkBtf2{mgg:|qrv}rPvm6R^ms>k~mvnQzLH1ORj6n8fmX_xo{>Ӏ[tuZnkhk\h{hS+|z.HeH=q3]mSYjkjidc|_~uopx{mg@bscwҎUVH`UfOEV}bnrigAgd^PwyH`etj7{OzaxnkVgi_Z~uRK}^h7fTo_RyN^VNūݙSXcgN\tpS_jrȫT{]^wvmsݪWzaowujoGsMsuT`TjXYha[l}|qabvKXn~iK9jRnuxxrpoxe}o=j_lyXvQ7we>WXSuJ{cg~}}}:TP_xj*qStswnQqe}1wJɰbMto=g4Jy^ltQys\zy|ELIGcSRk{c̀$Yyz[lhKW{qC|dU{~eo(lgkgbSrī^^pnKYfSiG_,iWw^ie?usVc)⏄{x}8,cTgxfu{s\mk^r[l~olymzFi^tt{ObnhSIvylA^{PNJu;jk4n4H^S\rXvovqaZiUZAh}zolcgkCctSogՁbmʋslMC}XCVp1Z+bqju\oNSwchdGju\{pDFd}``in[On֡sw†rqxwz{ǧ:`KsvEWzoZNxiuvarRi`vScp\oqnğJtaQGCtJß|mF~xqyWa~ff4u]uedzVY^bk{nTOYrzQLJzXPa^qo]YOspm~OIQifL}Oi`0q`xh`T[|kbl}xsykfqoFb_lG}S|зqfixylxbvRi^bDr}ajj|rN`*y|gmlknQgRWEixBfslitraGSlXnpZeawi AJ6kVosU/tm^v{Nj4~`ޖZOêuYv1ucQG{^/L~jwqXwjssex|e5Y/kȥb7VUx}gHa|h|b40NBzpXq_s^~~Quqe$ro=αn[O]Tg`|ZvmaReeioL'\dyer{T>uwp^ge[ijfszVq{CqLskx`pJw}wSpJVOhobPKCu~Tlq{urQwkficc[{MkxbRdenHpweoJ'exsuyXFZhuyBJcU:xŖqiltƓXgXawDfpsE~xoYdmjM]nRKe[w4}.ًJSw]anto}déjEljxrT-O{\rBc{Z_|g{ pzPX|]va4NlM@N?f^JWZs~n\{Ke~YlC4{?}cw^a^MjZqj4_mbWea`|PpS:h{ZyQqm$؛^Jcc~mbixe|V]mzS>"fƒnsFyK{rTamkbpiukiE{dguwiňWGTbwqXaezWL|XSqQJxUC$pEqQG_wCkvW}Lm]l:PYwJ|ъW[\YdD]~Axp~j9yAId\N}`y=z@fcnn~RdxlyUmp}lx@HzoAm`gv@hm;cx_zaY?EG~bgfdnxi:aoAqOX6yZ~yrsalP\>'|Cis}xR[X@tp~vybpqbEo{@e[I]hzoui}Mf_zef{[NRodnvb_>Ԛhmc|NGtPbkdmzLst`ewjGKgqTt5Ba*x\zx{b@~s~NqKY`tANwЀI{MS_[gYG?qH|q٥^N}^ߜFTxZgmM~rqifZde^nQzthSVj{^R`nqlzOVřftZ6PWƕ^`|>l11R`r/wffs}2Ul`Zl8eV\}eyWCowi|od~cłb^FwRfvxOD;~RxrtNh~mZpuM{~@pvZgze}ktO}XRd9uXxtbAhd\h+Q{lOIVZFskCq>ubV­_YmHis`~ZU}W|c}|rokTFT-nmq{TfxHq}Hdr~^噏dpkomHln`MQwt`tXuOa|}h0F~iz_W6vkbu0iOqmfb_jtTz{ʇuaHzNzfvUO\ogromlb~pgxuah[t}vp&p@uwyTkt?iysfi~zpqŕtye`MZ`NH\`gu4UIxHX~yjRvpdxJaTqcvPTT?VlLjztkpbvv[ cXRoivrhXS`Yٷor|^wn{v}k`e1~ke\OqsNZu}trzRxejkt@z\wm>h~^{ws%q̟xr|U~zy@a~VUJ[zrbApbup{d`zyB]|:ky`{_CkĎaCUEuէHItn[LS|yx|uz^ZacNG{mi{a|zt`j`|iYG^Y{Zg(adPy_zmS@ZxT][ieueIY̜zdNjPW[`FA{mzMmwxiJNeo`|G~dmvinxo_LyŎk"y`T&JimBwr{Mt`hcsTzivIfpy~f,jvn`~`c|`zRM԰p0N^j4tvX]Ȅ}^Q;f`r[emtff|`6qsfrmso.L~svrrFq[tfwdfvk{fm3^~p|]Pmtqmtgy|qcflItdH&|{q~adz_qr|V}GZX+vZdu|okijkęr?ISYVzbf`j{WEiZeq9@vzҎ{kjwLwh{s>XwtqdN_uJzqu_im}nh¤}JRtO{en^txu|x<^vi_zvLv`cp^lk_)Osl{ǟ{i||]\]]KnlT`PY]qAa{]oxa~I}]vv_qmugtqTMJfd}l}se{D{LTLgICFlfdgɥxntM}~i`fmyNTdhjZh]kJtcwzlcvf7]l}VqbU^pk9zlis[^yih}oyЙIWb2yoOb~W}x{vPjqy{l3e_}zb4qx~zec>EGZmrqt]^coy9vmc;[CkU5{^Śf8ocj_kdUi~ĝCV^lhrɌؓRi\vAsm|=}xxY[IUwhqT:Bj6_|0}'Z_|?ԜukRq3w`hZM,w]}hF/#vjc`}膓eLFiJhntyRRJS>l6x˃[]w}}Zt^{e-m:rrk)~xfvc{{QlqfxN_OrTi}kQky_x~iv˄jjH{f-zv-y҉C䉾mRr]pAOxV5X:3RoE@lbwGN[BPrV{kPExlL`bp7t}ဤfN0~%ffyibR`[us_Ir>H[{Y4zêvsmMoixǨF~S+Zu%gSlthNjuqĵXmfaW{oBUXQmC5ĿV׉~i\rp֌mZrohAiwYMqRy$ypVRaPG9X\TŪ_hiрa1MQώ|ɷ|F;Tk^OoYicEMSwmIed None: """Skip the current test unless a GPU is discoverable via PDH.""" @@ -78,8 +87,8 @@ def _require_npu() -> None: def _assert_hw_monitor_section( - data: dict, device_kind: str, *, require_utilization: bool = True -) -> None: + data: dict, device_kind: str, *, require_utilization: bool = True + ) -> None: """Assert the ``hw_monitor`` section is present and well-formed. Checks the section emitted by HWMonitor when --monitor is passed: @@ -97,6 +106,9 @@ def _assert_hw_monitor_section( assert hw["adapter_luid"] is not None if require_utilization: assert hw[device_kind]["mean_pct"] > 0 + else: + # Just a valid data + assert hw[device_kind]["mean_pct"] >= 0 def _build_perf_args( @@ -110,6 +122,10 @@ def _build_perf_args( memory: bool | None = None, verbose: bool = False, no_skip_build: bool = False, + batch_size: int | None = None, + input_data: Path | None = None, + op_tracing: str | None = None, + iterations_overwrite: int | None = None, ) -> list[str]: """Build the argv list passed to the perf CLI. @@ -117,7 +133,9 @@ def _build_perf_args( enough samples to observe utilization) and 3 otherwise (kept tiny for e2e speed). Warmup is always 1. """ - iterations = 300 if monitor else 3 + iterations = ( + iterations_overwrite if iterations_overwrite is not None else (300 if monitor else 3) + ) args: list[str] = [ "-m", model_arg, @@ -144,6 +162,12 @@ def _build_perf_args( args.append("--verbose") if no_skip_build: args.append("--no-skip-build") + if batch_size is not None: + args += ["--batch-size", str(batch_size)] + if input_data is not None: + args += ["--input-data", str(input_data)] + if op_tracing is not None: + args += ["--op-tracing", op_tracing] return args @@ -184,6 +208,28 @@ class _PerfBenchmarkSuite: def model_arg(self) -> str: raise NotImplementedError("Subclasses must override model_arg fixture") + @pytest.fixture + def npu_model_arg(self, model_arg: str) -> str: + """Model source used by the NPU tests. + + Defaults to ``model_arg`` so subclasses share a single model. + Subclasses backed by a real ONNX file override this to point at a + model that actually executes on the NPU (QNN / VitisAI), where a + bare float MatMul is not representative. + """ + return model_arg + + @pytest.fixture + def gpu_model_arg(self, model_arg: str) -> str: + """Model source used by the GPU tests. + + Defaults to ``model_arg`` so subclasses share a single model. + Subclasses backed by a real ONNX file override this to point at a + model that actually executes on the GPU, where a bare float MatMul + is not representative. + """ + return model_arg + def test_benchmark_cpu(self, tmp_path: Path, model_arg: str): """Benchmark on CPU with minimal iterations. @@ -340,7 +386,7 @@ def test_benchmark_cpu_no_memory(self, tmp_path: Path, model_arg: str): # Console output should NOT contain Memory section assert "Memory:" not in result.output - def test_benchmark_npu_memory(self, tmp_path: Path, model_arg: str): + def test_benchmark_npu_memory(self, tmp_path: Path, npu_model_arg: str): """Benchmark on NPU with --memory produces VRAM fields. Verifies VRAM local/shared fields are present in JSON output. @@ -352,7 +398,7 @@ def test_benchmark_npu_memory(self, tmp_path: Path, model_arg: str): result = runner.invoke( perf, _build_perf_args( - model_arg=model_arg, output_file=output_file, device="npu", memory=True + model_arg=npu_model_arg, output_file=output_file, device="npu", memory=True ), obj={}, catch_exceptions=False, @@ -401,7 +447,7 @@ def test_benchmark_cpu_monitor(self, tmp_path: Path, model_arg: str): data = json.loads(output_file.read_text()) _assert_monitor_result(data, device="cpu") - def test_benchmark_gpu_monitor(self, tmp_path: Path, model_arg: str): + def test_benchmark_gpu_monitor(self, tmp_path: Path, gpu_model_arg: str): """Benchmark on GPU with --monitor. Requires a real GPU discoverable via PDH. Verifies the JSON output @@ -415,7 +461,11 @@ def test_benchmark_gpu_monitor(self, tmp_path: Path, model_arg: str): result = runner.invoke( perf, _build_perf_args( - model_arg=model_arg, output_file=output_file, device="gpu", monitor=True + model_arg=gpu_model_arg, + output_file=output_file, + device="gpu", + monitor=True, + iterations_overwrite=1000, ), obj={}, catch_exceptions=False, @@ -424,10 +474,9 @@ def test_benchmark_gpu_monitor(self, tmp_path: Path, model_arg: str): assert output_file.exists(), f"Output file not created: {output_file}" data = json.loads(output_file.read_text()) - # Tiny synthetic fixture: below PDH utilization-publish floor. - _assert_monitor_result(data, device="gpu", require_utilization=False) + _assert_monitor_result(data, device="gpu") - def test_benchmark_npu_monitor(self, tmp_path: Path, model_arg: str): + def test_benchmark_npu_monitor(self, tmp_path: Path, npu_model_arg: str): """Benchmark on NPU with --monitor. Requires a real NPU discoverable via PDH. Verifies the JSON output @@ -441,7 +490,7 @@ def test_benchmark_npu_monitor(self, tmp_path: Path, model_arg: str): result = runner.invoke( perf, _build_perf_args( - model_arg=model_arg, output_file=output_file, device="npu", monitor=True + model_arg=npu_model_arg, output_file=output_file, device="npu", monitor=True ), obj={}, catch_exceptions=False, @@ -450,8 +499,7 @@ def test_benchmark_npu_monitor(self, tmp_path: Path, model_arg: str): assert output_file.exists(), f"Output file not created: {output_file}" data = json.loads(output_file.read_text()) - # Tiny synthetic fixture: below PDH utilization-publish floor. - _assert_monitor_result(data, device="npu", require_utilization=False) + _assert_monitor_result(data, device="npu") def test_benchmark_auto(self, tmp_path: Path, model_arg: str): """Benchmark with --device auto. @@ -528,7 +576,7 @@ def test_benchmark_ep_device_cpu(self, ep: str, tmp_path: Path, model_arg: str): _assert_monitor_result(data, device="cpu", ep=EP_ALIASES[ep]) @pytest.mark.parametrize("ep", GPU_EPS) - def test_benchmark_ep_device_gpu(self, ep: str, tmp_path: Path, model_arg: str): + def test_benchmark_ep_device_gpu(self, ep: str, tmp_path: Path, gpu_model_arg: str): """Benchmark with --ep and --device gpu. Skipped if the specified EP or a GPU is unavailable on the host. @@ -542,7 +590,12 @@ def test_benchmark_ep_device_gpu(self, ep: str, tmp_path: Path, model_arg: str): result = runner.invoke( perf, _build_perf_args( - model_arg=model_arg, output_file=output_file, device="gpu", ep=ep, monitor=True + model_arg=gpu_model_arg, + output_file=output_file, + device="gpu", + ep=ep, + monitor=True, + iterations_overwrite=1000, ), obj={}, catch_exceptions=False, @@ -551,11 +604,11 @@ def test_benchmark_ep_device_gpu(self, ep: str, tmp_path: Path, model_arg: str): assert output_file.exists() data = json.loads(output_file.read_text()) - # Tiny synthetic fixture: below PDH utilization-publish floor. - _assert_monitor_result(data, device="gpu", ep=EP_ALIASES[ep], require_utilization=False) + # openvino gpu could not emit valid pdh counter + _assert_monitor_result(data, device="gpu", ep=EP_ALIASES[ep], require_utilization=ep != "openvino") @pytest.mark.parametrize("ep", NPU_EPS) - def test_benchmark_ep_device_npu(self, ep: str, tmp_path: Path, model_arg: str): + def test_benchmark_ep_device_npu(self, ep: str, tmp_path: Path, npu_model_arg: str): """Benchmark with --ep and --device npu. Skipped if the specified EP or a NPU is unavailable on the host. @@ -569,7 +622,7 @@ def test_benchmark_ep_device_npu(self, ep: str, tmp_path: Path, model_arg: str): result = runner.invoke( perf, _build_perf_args( - model_arg=model_arg, output_file=output_file, device="npu", ep=ep, monitor=True + model_arg=npu_model_arg, output_file=output_file, device="npu", ep=ep, monitor=True ), obj={}, catch_exceptions=False, @@ -578,8 +631,7 @@ def test_benchmark_ep_device_npu(self, ep: str, tmp_path: Path, model_arg: str): assert output_file.exists() data = json.loads(output_file.read_text()) - # Tiny synthetic fixture: below PDH utilization-publish floor. - _assert_monitor_result(data, device="npu", ep=EP_ALIASES[ep], require_utilization=False) + _assert_monitor_result(data, device="npu", ep=EP_ALIASES[ep]) # =========================================================================== @@ -594,6 +646,127 @@ class TestPerfONNXDirect(_PerfBenchmarkSuite): def model_arg(self, onnx_model_path: Path) -> str: return str(onnx_model_path) + @pytest.fixture + def npu_model_arg(self, model_arg: str) -> str: + """NPU tests run against a real image-classification ONNX model. + + A bare float MatMul does not exercise a representative NPU + (QNN / VitisAI) execution path, so the NPU tests use the model + shipped under ``tests/assets/`` instead of ``model_arg``. + """ + return str(ASSETS_NPU_ONNX_MODEL) + + @pytest.fixture + def gpu_model_arg(self, model_arg: str) -> str: + """GPU tests run against a real image-classification ONNX model. + + A bare float MatMul does not exercise a representative GPU + execution path, so the GPU tests use the model shipped under + ``tests/assets/`` instead of ``model_arg``. + """ + return str(ASSETS_GPU_ONNX_MODEL) + + def test_batch_size_cpu(self, tmp_path: Path, onnx_model_path: Path): + """--batch-size applies to a model with a dynamic leading dimension.""" + model = onnx.load(onnx_model_path) + for value_info in (*model.graph.input, *model.graph.output): + dimensions = value_info.type.tensor_type.shape.dim + if dimensions: + dimensions[0].ClearField("dim_value") + dimensions[0].dim_param = "batch" + + dynamic_model_path = tmp_path / "dynamic_batch.onnx" + onnx.save(model, dynamic_model_path) + output_file = tmp_path / "perf_batch_size_cpu.json" + + result = CliRunner().invoke( + perf, + _build_perf_args( + model_arg=str(dynamic_model_path), + output_file=output_file, + device="cpu", + batch_size=4, + memory=False, + ), + obj={}, + catch_exceptions=False, + ) + + assert result.exit_code == 0, f"perf failed (exit {result.exit_code}):\n{result.output}" + data = json.loads(output_file.read_text()) + assert data["benchmark_info"]["batch_size"] == 4 + assert data["benchmark_info"]["effective_batch_size"] == 4 + assert all(shape[0] is None for shape in data["model_info"]["input_shapes"]) + assert data["throughput"]["samples_per_sec"] > 0 + + def test_input_data_cpu(self, tmp_path: Path, onnx_model_path: Path): + """--input-data benchmarks named tensors from an NPZ archive on CPU.""" + model = onnx.load(onnx_model_path) + initializer_names = {initializer.name for initializer in model.graph.initializer} + arrays = {} + for value_info in model.graph.input: + if value_info.name in initializer_names: + continue + tensor_type = value_info.type.tensor_type + shape = [dimension.dim_value or 1 for dimension in tensor_type.shape.dim] + dtype = onnx.helper.tensor_dtype_to_np_dtype(tensor_type.elem_type) + arrays[value_info.name] = np.ones(shape, dtype=dtype) + + input_file = tmp_path / "inputs.npz" + np.savez(input_file, **arrays) + output_file = tmp_path / "perf_input_data_cpu.json" + + result = CliRunner().invoke( + perf, + _build_perf_args( + model_arg=str(onnx_model_path), + output_file=output_file, + device="cpu", + input_data=input_file, + memory=False, + ), + obj={}, + catch_exceptions=False, + ) + + assert result.exit_code == 0, f"perf failed (exit {result.exit_code}):\n{result.output}" + data = json.loads(output_file.read_text()) + assert data["benchmark_info"]["device"] == "cpu" + effective_batch = next(iter(arrays.values())).shape[0] + assert data["benchmark_info"]["effective_batch_size"] == effective_batch + assert data["latency_ms"]["mean"] > 0 + + def test_op_tracing_basic_qnn_npu(self, tmp_path: Path, npu_model_arg: str): + """--op-tracing basic produces a QNN NPU operator trace.""" + require_ep("qnn") + _require_npu() + output_file = tmp_path / "perf_op_tracing_qnn_npu.json" + trace_output = tmp_path / "perf_op_tracing_qnn_npu_op_trace.json" + + result = CliRunner().invoke( + perf, + _build_perf_args( + model_arg=npu_model_arg, + output_file=output_file, + device="npu", + ep="qnn", + op_tracing="basic", + memory=False, + ), + obj={}, + catch_exceptions=False, + ) + + assert result.exit_code == 0, f"perf failed (exit {result.exit_code}):\n{result.output}" + assert output_file.exists() + assert trace_output.exists() + trace = json.loads(trace_output.read_text()) + assert trace["metadata"]["device"] == "npu" + assert trace["metadata"]["ep"] == EP_ALIASES["qnn"] + assert trace["metadata"]["tracing_level"] == "basic" + assert trace["metadata"]["num_samples"] == 3 + assert trace["operators"] + class TestPerfHuggingFace: """Benchmark a HuggingFace model by loading it via the perf command.""" @@ -637,7 +810,12 @@ def test_benchmark_ep_gpu(self, ep: str, tmp_path: Path, model_arg: str): result = runner.invoke( perf, _build_perf_args( - model_arg=model_arg, output_file=output_file, device="gpu", ep=ep, monitor=True + model_arg=model_arg, + output_file=output_file, + device="gpu", + ep=ep, + monitor=True, + iterations_overwrite=1000, ), obj={}, catch_exceptions=False, @@ -647,9 +825,7 @@ def test_benchmark_ep_gpu(self, ep: str, tmp_path: Path, model_arg: str): assert output_file.exists() data = json.loads(output_file.read_text()) assert data["benchmark_info"]["ep"] == EP_ALIASES[ep] - # Not all EPs bump PDH GPU-engine counters (OpenVINO routes via its own - # compute path); validate structure only, not utilization magnitude. - _assert_monitor_result(data, device="gpu", require_utilization=False) + _assert_monitor_result(data, device="gpu") @pytest.mark.parametrize("ep", NPU_EPS) def test_benchmark_ep_npu(self, ep: str, tmp_path: Path, model_arg: str): @@ -673,9 +849,7 @@ def test_benchmark_ep_npu(self, ep: str, tmp_path: Path, model_arg: str): assert output_file.exists() data = json.loads(output_file.read_text()) assert data["benchmark_info"]["ep"] == EP_ALIASES[ep] - # Not all EPs bump PDH NPU-engine counters reliably for short runs; - # validate structure only, not utilization magnitude. - _assert_monitor_result(data, device="npu", require_utilization=False) + _assert_monitor_result(data, device="npu") # ===========================================================================