Repository navigation
Expand file tree
/
Copy pathrun_flagsparse_pytest.py
More file actions
5103 lines (4674 loc) · 177 KB
/
Copy pathrun_flagsparse_pytest.py
File metadata and controls
5103 lines (4674 loc) · 177 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
# Copyright 2026 FlagOS Contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Run FlagSparse accuracy and performance suites per operator.
The operator inventory comes from ``conf/operators.yaml`` by default. Each
configured operator is run as an isolated subprocess on one requested GPU, with
accuracy first and performance second, then all results are summarized together.
"""
from __future__ import annotations
import argparse
import csv
import datetime as _dt
import html
import importlib
import importlib.metadata as importlib_metadata
import json
import math
import os
import platform
import re
import shlex
import signal
import statistics
import subprocess
import sys
import threading
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass
from pathlib import Path
from tools import baseline_bound as _baseline_bound
from tools.delivery_variants import (
cleanup_delivery_matrices,
load_delivery_variants,
select_delivery_matrices,
)
# XPU compiler diagnostics can include an MLIR reproducer and exceed the
# csv module's conservative 128 KiB default. They are part of the execution
# result, so retaining them must not turn a TRITON_COMPILE row into PASS.
try:
csv.field_size_limit(sys.maxsize)
except OverflowError:
csv.field_size_limit(2**31 - 1)
try:
import yaml
except Exception:
yaml = None
try:
from openpyxl import Workbook
except Exception:
Workbook = None
ANSI_RE = re.compile(r"\x1B\[[0-?]*[ -/]*[@-~]")
SUMMARY_RE = re.compile(r"(\d+)\s+([A-Za-z_]+)")
PYTEST_CASE_RE = re.compile(
r"^(?P<nodeid>\S+::\S+)\s+"
r"(?P<status>PASSED|FAILED|SKIPPED|ERROR|XFAIL|XPASS)\b"
r"(?P<message>.*)$"
)
SUMMARY_LOCK = threading.Lock()
TIMEOUT_RETURN_CODE = -100
DEFAULT_EXCLUDED_OPS = {
"alpha_spmm_alg1",
"spmv_coo_tocsr",
"spsv_descriptor_api",
"sparse_format_constructors",
# spmm_csr now routes to the optimized (alg1) kernel by default, so the
# standalone opt operators are dropped from the default CI run. They remain
# configured and can still be run explicitly via --ops.
"spmm_csr_opt",
"spmm_csr_opt_alg1",
"spmm_csr_opt_alg2",
}
STATUS_TO_FLAGGEMS = {
"PASS": "Passed",
"FAIL": "Failed",
"SKIP": "Skipped",
# Capability-probe verdicts. A kernel the backend declined is a skip; one
# that would not compile is a failure -- neither may read as a pass.
"REJECTED": "Skipped",
"TRITON_COMPILE": "Failed",
"MISMATCH": "Failed",
"ERROR": "Error",
"MIXED": "Error",
"TIMEOUT": "Timeout",
"NO_TESTS": "NotFound",
"CRASH": "Error",
"NOT_CONFIGURED": "NotFound",
"Passed": "Passed",
"Failed": "Failed",
"Skipped": "Skipped",
"Timeout": "Timeout",
"NotFound": "NotFound",
"Error": "Error",
# Kernel ran and passed, but no vendor baseline exists for this dtype.
# Emitted by capi/tools/write_summary.py; kept here so the two tables match.
"NoBaseline": "NoBaseline",
}
# Benchmark arguments that narrow each delivery parent's default CSV sweep to
# what the delivery variants actually name: int32 indices and the `non` op. The
# default sweeps also cover int64 and trans/conj (spmm_csr: 720 rows, 120 of
# them delivery), which on MetaX C550 ran past a 900 s --timeout. Injected only
# under --delivery-only, BEFORE --benchmark-args/--op-benchmark-args, so an
# explicit user flag still wins (argparse keeps the last value). Parents absent
# here (sddmm_csr) already sweep only delivery axes.
#
# The dtype list is spelled out too, in both directions. gather/scatter deliver
# every dtype the script knows. spmv_csr/spmv_coo/spmm_csr/spmm_coo deliver only
# float32 and float64 (the complex variants left the delivery list on 2026-09-21),
# while the scripts sweep complex by default; benchmarking it would only produce
# rows no delivery variant reads. When a dtype is added to the delivery list, add
# it here as well: a script whose default omits it reports NotFound for the
# variant ("the benchmark recorded no <dtype> rows"), which reads like a pass.
DELIVERY_BENCHMARK_ARGS: dict[str, tuple[str, ...]] = {
"gather": (
"--index-dtypes",
"int32",
"--value-dtypes",
"float16,float32,float64,complex64,complex128",
),
"scatter": ("--index-dtypes", "int32"),
"spmv_csr": (
"--index-dtype",
"int32",
"--ops",
"non",
"--dtypes",
"float32,float64",
),
"spmv_coo": (
"--index-dtypes",
"int32",
"--ops",
"non",
"--dtypes",
"float32,float64",
),
"spmm_csr": (
"--index-dtypes",
"int32",
"--ops",
"non",
"--dtypes",
"float32,float64",
),
"spmm_coo": ("--index-dtypes", "int32", "--dtypes", "float32,float64"),
}
# SpSV is not a delivery parent any more (the delivery list dropped it on
# 2026-09-21), so `--delivery-only` no longer selects spsv_csr/spsv_coo and this
# narrowing only applies when they are selected explicitly, e.g. `--ops spsv_csr
# --delivery-only` to re-check the int32 / NON / non-unit contract it used to have.
#
# That contract: the shared pytest files also exercise transpose/conjugate and
# unit-diagonal APIs -- valuable full-suite coverage, but not int32 NON non-unit.
# They have to be left out of the run itself, not only out of the recorded result:
# on a C550 the unit-diagonal cases reach the known CW defect (illegal access /
# hang), so a run that merely filtered afterwards would still execute them.
#
# pytest `-k` matches substrings of the test name and its parameter ids. "non_trans"
# keeps the lower and the upper NON tests (`..._non_trans_supported_combos`,
# `..._non_trans_upper_supported_combos`; a longer keyword such as
# "non_trans_supported_combos" would silently drop the upper ones), and "not unit"
# drops every unit-diagonal test (`..._non_trans_unit_supported_combos`,
# `..._unit_transpose_family_...`). It goes in front of --pytest-args, so an
# explicit `-k` from the user still wins.
_DELIVERY_SPSV_PYTEST_FILTER = "non_trans and int32 and not unit"
def _delivery_accuracy_pytest_args(op: str, delivery_only: bool) -> list[str]:
"""Extra pytest arguments that narrow an accuracy run to the delivery contract."""
if delivery_only and op in {"spsv_csr", "spsv_coo"}:
return ["-k", _DELIVERY_SPSV_PYTEST_FILTER]
return []
PYTEST_STATUS_TO_FLAGGEMS = {
"PASSED": "Passed",
"FAILED": "Failed",
"SKIPPED": "Skipped",
"ERROR": "Error",
"XFAIL": "Skipped",
"XPASS": "Passed",
}
PERFORMANCE_METRIC_COLUMNS = {
"latency_base",
"latency",
"speedup",
"triton_ms",
"pytorch_ms",
"cusparse_ms",
"cupy_ms",
"csc_ms",
"bsr_ms",
"base_ms",
"alg1_ms",
"alg2_ms",
"analysis_ms",
"solve_ms",
"total_ms",
"max_abs_err",
"max_rel_err",
}
# VENDOR FIRST, PyTorch as the fallback. The runner reports the first non-empty
# match, so within every family the vendor pair is listed ahead of the PyTorch
# pair: on CUDA and ROCm the number is FlagSparse against cuSPARSE / hipSPARSE,
# and only an operator the vendor library does not implement falls through to
# the PyTorch metric. A backend with no vendor column at all -- MACA, MUSA, XPU --
# falls through for every operator, which is the same rule, not an exception.
#
# This ordering used to be the other way round for the `triton_*` family while a
# comment further down claimed the vendor metric won; it did not, and every CUDA
# speedup in the delivery report was against PyTorch.
#
# A speedup name can appear more than once with different column layouts --
# spmm_coo reports triton_speedup_vs_pytorch over torch_ms/ms while spmm_csc uses
# pytorch_ms/ms -- so each layout is listed and _performance_schema prefers the
# entry whose measurement columns the row actually carries.
PERFORMANCE_SPEEDUP_SCHEMAS = (
# benchmark_ascend.py names its ratio plainly `speedup` over pytorch_ms/triton_ms.
# Ahead of the generic `speedup` entry below: without a schema of its own every
# Ascend row fell through to latency_base/latency, which that CSV does not carry,
# so _performance_row_has_complete_speedup rejected all of them and each dtype
# aggregate stayed 0 -- printed as `-` while the CSV held a full measurement.
("speedup", "pytorch_ms", "triton_ms"),
("speedup", "latency_base", "latency"),
("speedup_vs_vendor", "vendor_ms", "ms"),
("triton_speedup_vs_cusparse", "cusparse_ms", "triton_ms"),
("triton_speedup_vs_cupy", "cupy_ms", "triton_ms"),
# spmm_coo carries BOTH triton_speedup_vs_pytorch (over torch_ms) and
# cusparse_vs_alg_speedup (over cusparse_ms). The vendor one has to be ahead
# of the PyTorch ones or that operator alone keeps reporting against torch
# while everything else reports against cuSPARSE -- vendor-first holding for
# ten operators and quietly not for the eleventh.
("cusparse_vs_alg2_speedup", "cusparse_ms", "alg2_ms"),
("cusparse_vs_alg1_speedup", "cusparse_ms", "alg1_ms"),
("cusparse_vs_alg_speedup", "cusparse_ms", "ms"),
("triton_speedup_vs_pytorch", "pytorch_ms", "triton_ms"),
("triton_speedup_vs_pytorch", "pytorch_ms", "ms"),
("triton_speedup_vs_pytorch", "torch_ms", "ms"),
("triton_speedup_vs_pytorch_api", "pytorch_api_ms", "triton_ms"),
("csc_speedup_vs_cusparse", "cusparse_ms", "csc_ms"),
("csc_speedup_vs_pytorch", "pytorch_ms", "csc_ms"),
("bsr_speedup_vs_cusparse", "cusparse_ms", "bsr_ms"),
("bsr_speedup_vs_pytorch", "pytorch_ms", "bsr_ms"),
("opt_speedup_vs_cusparse", "cusparse_ms", "opt_ms"),
("opt_speedup_vs_pytorch", "pytorch_ms", "opt_ms"),
("opt_vs_base", "base_ms", "opt_ms"),
("base_vs_alg2_speedup", "base_ms", "alg2_ms"),
("base_vs_alg1_speedup", "base_ms", "alg1_ms"),
("torch_vs_alg2_speedup", "torch_ms", "alg2_ms"),
("torch_vs_alg1_speedup", "torch_ms", "alg1_ms"),
("torch_vs_alg_speedup", "torch_ms", "ms"),
("cusparse_speedup_solve", "cusparse_ms", "solve_ms"),
("pytorch_speedup_solve", "pytorch_ms", "solve_ms"),
("cusparse_speedup_total", "cusparse_ms", "triton_total_ms"),
("pytorch_speedup_total", "pytorch_ms", "triton_total_ms"),
("scipy_vs_alg_speedup", "scipy_cpu_ms", "ms"),
("prepared_speedup_vs_pytorch", "pytorch_ms", "prepared_ms"),
# SpSV and SpSM spell their columns in the vendor's own casing, and SpSV
# names the CuPy route explicitly. Column lookup is case-insensitive (see
# _row_value), so "cuSPARSE_ms" resolves against "cusparse_ms" here, but a
# DIFFERENT name still needs its own entry -- which is why the two below
# exist and why these operators used to report a speedup with both
# latencies reading 0.00 ms.
("FlagSparse_vs_CuPy/cuSPARSE_speedup", "CuPy/cuSPARSE_ms", "flagsparse_ms"),
("FlagSparse_vs_cuSPARSE_speedup", "cusparse_ms", "flagsparse_ms"),
# SpSV names its columns after the ACTIVE vendor label, so DCU writes
# hipSPARSE_ms and FlagSparse_vs_hipSPARSE_all_speedup (the `_all` suffix is
# ROCm-only: that path reports bufferSize/analysis/solve separately). Without
# these two entries the DCU rows matched nothing and fell through to the
# PyTorch metric -- vendor-first would have held on CUDA and quietly not on DCU.
("FlagSparse_vs_hipSPARSE_all_speedup", "hipsparse_ms", "flagsparse_ms"),
("FlagSparse_vs_hipSPARSE_speedup", "hipsparse_ms", "flagsparse_ms"),
("FlagSparse_vs_vendor_speedup", "cusparse_ms", "flagsparse_ms"),
("FlagSparse_vs_vendor_speedup", "hipsparse_ms", "flagsparse_ms"),
# Backends whose vendor is torch.sparse, or which have none, write a neutral
# `vendor_ms` rather than a column named after the PyTorch label or "N/A".
("FlagSparse_vs_vendor_speedup", "vendor_ms", "flagsparse_ms"),
("FlagSparse_vs_PyTorch_all_speedup", "pytorch_ms", "flagsparse_ms"),
("FlagSparse_vs_PyTorch_speedup", "pytorch_ms", "flagsparse_ms"),
# LAST, so any vendor or PyTorch speedup a row has wins. Rows with neither get
# theirs from _apply_scaled_baseline: the same case's H800 cuSPARSE time,
# rescaled to this card by the bottleneck-unit peak ratio, is the baseline (1.0).
("speedup_vs_h800_scaled", "h800_scaled_ms", "h800_scaled_fs_ms"),
)
@dataclass(frozen=True)
class OperatorTestConfig:
accuracy_marker: str | None = None
performance_cmd: tuple[str, ...] | None = None
PERFORMANCE_COMMANDS: dict[str, tuple[str, ...]] = {
# Sparse-vector operators take synthetic (dense, nnz) cases, like gather/scatter.
**{
op: (
"tests/test_vector_ops.py",
"--op",
op,
"--csv-summary",
"{csv}",
"--warmup",
"{warmup}",
"--iters",
"{iters}",
)
for op in ("axpby", "spvv")
},
"spmv_sell": (
"tests/test_spmv_sell.py",
"{input}",
"--csv",
"{csv}",
"--warmup",
"{warmup}",
"--iters",
"{iters}",
),
"gather": (
"tests/test_gather.py",
"--csv-summary",
"{csv}",
"--warmup",
"{warmup}",
"--iters",
"{iters}",
),
"scatter": (
"tests/test_scatter.py",
"--csv-summary",
"{csv}",
"--warmup",
"{warmup}",
"--iters",
"{iters}",
),
# `auto`, not `compare`: the delivery number is the path a caller actually
# gets (row_tile on ROCm, legacy_segbin on CUDA, ...). `compare` writes one
# row per registered algorithm, and _is_delivery_performance_row drops those
# -- pass `--op-benchmark-args 'spmv_csr=--alg compare'` to tune, not deliver.
"spmv_csr": (
"tests/test_spmv_csr.py",
"{input}",
"--csv-csr",
"{csv}",
"--alg",
"auto",
"--warmup",
"{warmup}",
"--iters",
"{iters}",
),
"spmv_coo": (
"tests/test_spmv_coo.py",
"{input}",
"--csv-coo",
"{csv}",
"--warmup",
"{warmup}",
"--iters",
"{iters}",
),
"spmv_csc": (
"tests/test_spmv_csc.py",
"{input}",
"--csv-csc",
"{csv}",
"--warmup",
"{warmup}",
"--iters",
"{iters}",
),
"spmv_bsr": (
"tests/test_spmv_bsr.py",
"{input}",
"--csv-bsr",
"{csv}",
"--ops",
"non,trans,conj",
"--alg",
"compare",
"--warmup",
"{warmup}",
"--iters",
"{iters}",
),
"spmv_coo_tocsr": (
"tests/test_spmv_coo.py",
"{input}",
"--csv-tocsr",
"{csv}",
"--dtypes",
"float32,float64",
"--ops",
"non",
"--warmup",
"{warmup}",
"--iters",
"{iters}",
),
"spmm_csr": (
"tests/test_spmm.py",
"{input}",
"--csv",
"{csv}",
"--warmup",
"{warmup}",
"--iters",
"{iters}",
),
"spmm_coo": (
"tests/test_spmm_coo.py",
"{input}",
"--csv",
"{csv}",
"--warmup",
"{warmup}",
"--iters",
"{iters}",
),
"spmm_bsr": (
"tests/test_spmm_bsr.py",
"{input}",
"--csv-bsr",
"{csv}",
"--ops",
"non,trans,conj",
"--block-dims",
"2",
"--warmup",
"{warmup}",
"--iters",
"{iters}",
),
"spmm_bell": (
"tests/test_spmm_bell.py",
"{input}",
"--csv-bell",
"{csv}",
"--ops",
"non",
"--block-dims",
"2",
"--warmup",
"{warmup}",
"--iters",
"{iters}",
),
"spmm_csc": (
"tests/test_spmm_csc.py",
"{input}",
"--csv-csc",
"{csv}",
"--ops",
"non,trans,conj",
"--warmup",
"{warmup}",
"--iters",
"{iters}",
),
"spmm_csr_opt": (
"tests/test_spmm_opt.py",
"{input}",
"--csv",
"{csv}",
"--warmup",
"{warmup}",
"--iters",
"{iters}",
),
"spmm_csr_opt_alg1": (
"tests/test_spmm_opt.py",
"{input}",
"--csv",
"{csv}",
"--warmup",
"{warmup}",
"--iters",
"{iters}",
),
"spmm_csr_opt_alg2": (
"tests/test_spmm_opt_alg2.py",
"--synthetic",
"--csv",
"{csv}",
"--warmup",
"{warmup}",
"--iters",
"{iters}",
),
"alpha_spmm_alg1": (
"tests/test_alpha_spmm_alg1.py",
"--synthetic",
"--csv",
"{csv}",
"--warmup",
"{warmup}",
"--iters",
"{iters}",
),
"spgemm_csr": (
"tests/test_spgemm.py",
"{input}",
"--csv",
"{csv}",
"--warmup",
"{warmup}",
"--iters",
"{iters}",
),
"sddmm_csr": (
"tests/test_sddmm.py",
"{input}",
"--csv",
"{csv}",
"--warmup",
"{warmup}",
"--iters",
"{iters}",
),
"spsv_csr": (
"tests/test_spsv.py",
"{input}",
"--csv-csr",
"{csv}",
"--warmup",
"{warmup}",
"--iters",
"{iters}",
),
"spsv_coo": (
"tests/test_spsv.py",
"{input}",
"--csv-coo",
"{csv}",
"--warmup",
"{warmup}",
"--iters",
"{iters}",
),
"spsv_sell": (
"tests/test_spsv_sell.py",
"{input}",
"--csv",
"{csv}",
"--warmup",
"{warmup}",
"--iters",
"{iters}",
),
"spsm_csr": (
"tests/test_spsm.py",
"{input}",
"--csv-csr",
"{csv}",
"--warmup",
"{warmup}",
"--iters",
"{iters}",
),
"spsm_coo": (
"tests/test_spsm.py",
"{input}",
"--csv-coo",
"{csv}",
"--warmup",
"{warmup}",
"--iters",
"{iters}",
),
}
# Ascend uses its own entry points instead of the CUDA-oriented per-operator
# scripts. Selected only when FLAGSPARSE_BACKEND=ascend; every other backend keeps
# PERFORMANCE_COMMANDS untouched.
#
# Two of them, and the split is deliberate:
#
# benchmark_ascend.py measures against a torch-npu baseline for the
# seven supported delivery operators. Where a
# baseline exists, a measured speedup beats a bare
# timing.
# benchmark_ascend_probe.py runs every other operator over the 20-matrix
# spread and classifies the outcome -- PASS,
# REJECTED, TRITON_COMPILE, MISMATCH, ERROR. On a
# backend where kernels may not lower at all, "did
# it run, and if not why" is the measurement that
# matters, and the previous mapping simply had no
# entry for these operators: every one of them came
# back as "no performance command mapping", which
# reads the same as a pass.
# ---------------------------------------------------------------------------
# Backend registry, read from the library rather than restated here.
#
# The harness has to run on CUDA and on every domestic accelerator, and the list
# of those is the library's business, not the runner's. Importing it means a
# platform added to flagsparse reaches the accuracy and performance phases
# without a second edit here -- and, more to the point, cannot silently disagree
# with what the operators themselves detect.
#
# The fallback list exists so the runner still works against a checkout whose
# library predates the registry; it is deliberately the same order.
# ---------------------------------------------------------------------------
def _load_backend_names() -> tuple[str, ...]:
try:
sys.path.insert(0, str(Path(__file__).resolve().parent / "src"))
from flagsparse.sparse_operations._common import backend_specs
return tuple(spec.name for spec in backend_specs())
except Exception:
return ("cuda", "rocm", "metax", "mthreads", "ascend", "xpu", "gcu", "iluvatar")
SUPPORTED_BACKENDS: tuple[str, ...] = _load_backend_names()
# Backends whose operators go through the generic CUDA-oriented per-operator
# benchmark scripts. A backend absent from here needs its own entry below --
# and the point of listing them is that "not yet wired up" becomes visible
# instead of arriving as an empty result.
GENERIC_BENCHMARK_BACKENDS: tuple[str, ...] = (
"cuda",
"rocm",
"metax",
"mthreads",
"iluvatar",
)
# Backends with no per-operator benchmark of their own yet. They fall back to
# the capability probe, which reports PASS / REJECTED / TRITON_COMPILE per
# operator -- on a platform where a kernel may not lower at all, that is the
# measurement that matters, and it beats reporting nothing.
PROBE_ONLY_BACKENDS: tuple[str, ...] = ("gcu",)
# The XPU SDK has no cuSPARSE-shaped generic sparse API, so these seven run
# through benchmark_xpu.py instead of the probe. The script compares each
# FlagSparse result with an equivalent PyTorch-XPU expression and reports both
# latencies and their ratio; it is not a vendor/XDNN sparse-library comparison.
XPU_BASELINE_OPS: tuple[str, ...] = (
"gather",
"scatter",
"spmv_csr",
"spmv_coo",
"spmm_csr",
"spmm_coo",
"sddmm_csr",
)
# These are the XPU dtypes whose PyTorch expression is both executable and a
# valid comparison. The shim accepts float64 but computes it as float32, while
# its complex index primitives are unsupported.
XPU_BASELINE_DTYPES: dict[str, str] = {
"gather": "float16,float32",
"scatter": "float16,float32",
"spmv_csr": "float32",
"spmv_coo": "float32",
"spmm_csr": "float32",
"spmm_coo": "float32",
"sddmm_csr": "float32",
}
ASCEND_BASELINE_OPS: tuple[str, ...] = (
"gather",
"scatter",
"spmv_csr",
"spmm_csr",
"sddmm_csr",
)
ASCEND_BASELINE_DTYPES: dict[str, str] = {
op: (
"float16,float32,float64,complex64,complex128"
if op in ("gather", "scatter")
else "float16,float32,float64"
)
for op in ASCEND_BASELINE_OPS
}
# The COO delivery variants now have executable PyTorch-NPU index_add baselines.
# Keep them out of ASCEND_ACCURACY_OPS below: their established pytest accuracy
# path remains unchanged while performance switches from a capability probe to a
# measured, correctness-gated comparison.
ASCEND_PYTORCH_PERFORMANCE_OPS: tuple[str, ...] = (
*ASCEND_BASELINE_OPS,
"spmv_coo",
"spmm_coo",
"spmv_csc",
"spmm_csc",
)
ASCEND_PERFORMANCE_DTYPES: dict[str, str] = {
**ASCEND_BASELINE_DTYPES,
# scatter_i8_int is a delivery variant. benchmark_ascend.py handles the
# int8 value path directly; keep it in the Ascend-only dtype sweep so the
# untagged row can be projected onto that delivery variant.
"scatter": "float16,float32,float64,complex64,complex128,int8",
"spmv_coo": "float32,float64",
"spmm_coo": "float32,float64",
"spmv_csc": "float32",
"spmm_csc": "float32",
}
# The operators whose accuracy runs through benchmark_ascend_accuracy.py instead
# of the pytest suite. Deliberately DERIVED from ASCEND_BASELINE_OPS rather than
# repeating the same five names: the two sets have to move together, and the drop
# this came from carried a second hardcoded copy that would drift the first time
# either list changed.
ASCEND_ACCURACY_OPS: frozenset[str] = frozenset(ASCEND_BASELINE_OPS)
ASCEND_PROBE_OPS: tuple[str, ...] = (
"spmv_coo",
"spmv_csc",
"spmv_bsr",
"spmv_coo_tocsr",
"spmm_coo",
"spmm_csc",
"spmm_bsr",
"spmm_bell",
"spmm_csr_opt",
"spmm_csr_opt_alg1",
"spmm_csr_opt_alg2",
"alpha_spmm_alg1",
"spgemm_csr",
"spsv_csr",
"spsv_coo",
"spsv_sell",
"spsm_csr",
"spsm_coo",
)
ASCEND_Q4_VARIANTS: dict[str, tuple[str, ...]] = {
# Keep the measured rollout explicit; expand only after real-NPU validation.
"spmv_csr": (
"spmv_csr_f16_int_non",
"spmv_csr_f16f32_int_non",
"spmv_csr_f32c32_int_non",
"spmv_csr_c32_int_non",
"spmv_csr_f32_int_trans",
"spmv_csr_i8f32_int_non",
"spmv_csr_i8i32_int_non",
"spmv_csr_c32_int_conj",
),
"spmv_coo": (
"spmv_coo_f16_int_non",
"spmv_coo_f16f32_int_non",
"spmv_coo_f32_int_trans",
"spmv_coo_c32_int_non",
"spmv_coo_c32_int_conj",
"spmv_coo_i8i32_int_non",
),
"spmm_csr": (
"spmm_csr_f16f32_int_non_non_row",
"spmm_csr_f16_int_non_non_row",
"spmm_csr_f32_int_non_non_col",
"spmm_csr_f32_int_non_trans_row",
"spmm_csr_c32_int_non_non_row",
"spmm_csr_i8i32_int_non_non_row",
"spmm_csr_f32_int_trans_non_row",
),
"spmm_coo": (
"spmm_coo_c32_int_non_non_row",
"spmm_coo_f16_int_non_non_row",
"spmm_coo_i8i32_int_non_non_row",
),
"sddmm_csr": (
"sddmm_csr_f32_int_non_non_col",
"sddmm_csr_f32_int_non_trans_row",
"sddmm_csr_f32_int_trans_non_row",
"sddmm_csr_c32_int_non_non_row",
"sddmm_csr_f16_int_non_non_row",
),
"spmv_csc": (
"spmv_csc_f32_int_non",
"spmv_csc_c32_int_non",
"spmv_csc_f16_int_non",
),
"spmm_csc": (
"spmm_csc_f32_int_non_non_row",
"spmm_csc_c32_int_non_non_row",
"spmm_csc_f16_int_non_non_row",
),
}
# Every operator, routed to the capability probe. Used by any backend that has
# no per-operator benchmark of its own; the probe itself is backend-neutral and
# resolves the device from the same registry.
PROBE_PERFORMANCE_COMMANDS: dict[str, tuple[str, ...]] = {}
def _xpu_baseline_command(op: str) -> tuple[str, ...]:
"""Measured baseline for the XPU operators PyTorch can express."""
return (
"benchmark/benchmark_xpu.py",
"--op",
op,
"--dtypes",
XPU_BASELINE_DTYPES[op],
"--device",
"{device}",
# XPU baselines are isolated one matrix per subprocess by
# _run_bell_per_matrix(), so `{input}` is a file on this path.
"--matrix",
"{input}",
"--csv-summary",
"{csv}",
"--warmup",
"{warmup}",
"--iters",
"{iters}",
)
# Both delivery dtypes, for every probe command below. The probe itself defaults
# to float32 alone, and DELIVERY_BENCHMARK_ARGS cannot narrow a probe command --
# _uses_generic_benchmark_script() is False for these operators, by design, since
# the delivery flags belong to the per-operator scripts. So without passing this
# here the f64 variant of a probed operator had no row at all, and the delivery
# report showed it as NotFound: the same cell it uses for "never ran".
PROBE_DTYPE_ARGS: tuple[str, ...] = ("--dtypes", "float32,float64")
def _probe_command(op: str) -> tuple[str, ...]:
return (
"benchmark/benchmark_ascend_probe.py",
"--op",
"{op}",
"--device",
"{device}",
"--csv-summary",
"{csv}",
*PROBE_DTYPE_ARGS,
"--warmup",
"{warmup}",
"--iters",
"{iters}",
)
ASCEND_PERFORMANCE_COMMANDS: dict[str, tuple[str, ...]] = {
**{
op: (
"benchmark/benchmark_ascend.py",
"--op",
"{op}",
# The .mtx file or directory from --benchmark-input; without it the
# script silently measured its built-in synthetic case instead.
"--input",
"{input}",
"--device",
"{device}",
"--csv-summary",
"{csv}",
# Complex rows make unsupported Ascend gather/scatter capability explicit.
"--dtypes",
ASCEND_PERFORMANCE_DTYPES[op],
"--warmup",
"{warmup}",
"--iters",
"{iters}",
*(
("--q4-variants", ",".join(ASCEND_Q4_VARIANTS[op]))
if op in ASCEND_Q4_VARIANTS
else ()
),
)
for op in ASCEND_PYTORCH_PERFORMANCE_OPS
},
**{
op: (
"benchmark/benchmark_ascend_probe.py",
"--op",
"{op}",
"--device",
"{device}",
"--csv-summary",
"{csv}",
*PROBE_DTYPE_ARGS,
"--warmup",
"{warmup}",
"--iters",
"{iters}",
)
for op in ASCEND_PROBE_OPS
if op not in ASCEND_PYTORCH_PERFORMANCE_OPS
},
# These scripts are backend-neutral (their PyTorch baseline runs on
# torch_npu; the cuSPARSE/cupy column comes back empty with a reason via
# tests/cusparse_generic_baseline.py's skip_reason(), same as every other
# non-CUDA backend), so Ascend reuses them as-is rather than needing its own
# benchmark_ascend.py routing. spgemm_csr was on the probe, which times
# synthetic shapes (tiny_square, square_1k, ...) instead of the delivery
# matrices -- no speedup and nothing an H800 reference could be matched to.
**{
op: PERFORMANCE_COMMANDS[op]
for op in ("axpby", "spvv", "spmv_sell", "spgemm_csr")
},
}
OP_TEST_CONFIGS: dict[str, OperatorTestConfig] = {
"spmv_sell": OperatorTestConfig("spmv_sell", PERFORMANCE_COMMANDS["spmv_sell"]),
"axpby": OperatorTestConfig("axpby", PERFORMANCE_COMMANDS["axpby"]),
"spvv": OperatorTestConfig("spvv", PERFORMANCE_COMMANDS["spvv"]),
"gather": OperatorTestConfig("gather", PERFORMANCE_COMMANDS["gather"]),
"scatter": OperatorTestConfig("scatter", PERFORMANCE_COMMANDS["scatter"]),
"spmv_csr": OperatorTestConfig("spmv_csr", PERFORMANCE_COMMANDS["spmv_csr"]),
"spmv_coo": OperatorTestConfig("spmv_coo", PERFORMANCE_COMMANDS["spmv_coo"]),
"spmv_csc": OperatorTestConfig("spmv_csc", PERFORMANCE_COMMANDS["spmv_csc"]),
"spmv_bsr": OperatorTestConfig("spmv_bsr", PERFORMANCE_COMMANDS["spmv_bsr"]),
"spmv_coo_tocsr": OperatorTestConfig(
"spmv_coo_tocsr", PERFORMANCE_COMMANDS["spmv_coo_tocsr"]
),
"spmm_csr": OperatorTestConfig("spmm_csr", PERFORMANCE_COMMANDS["spmm_csr"]),
"spmm_coo": OperatorTestConfig("spmm_coo", PERFORMANCE_COMMANDS["spmm_coo"]),
"spmm_bsr": OperatorTestConfig("spmm_bsr", PERFORMANCE_COMMANDS["spmm_bsr"]),
"spmm_bell": OperatorTestConfig("spmm_bell", PERFORMANCE_COMMANDS["spmm_bell"]),
"spmm_csc": OperatorTestConfig("spmm_csc", PERFORMANCE_COMMANDS["spmm_csc"]),
"spmm_csr_opt": OperatorTestConfig(
"spmm_csr_opt", PERFORMANCE_COMMANDS["spmm_csr_opt"]
),
"spmm_csr_opt_alg1": OperatorTestConfig(
"spmm_csr_opt_alg1", PERFORMANCE_COMMANDS["spmm_csr_opt_alg1"]
),
"spmm_csr_opt_alg2": OperatorTestConfig(
"spmm_csr_opt_alg2", PERFORMANCE_COMMANDS["spmm_csr_opt_alg2"]
),
"alpha_spmm_alg1": OperatorTestConfig(
"alpha_spmm_alg1", PERFORMANCE_COMMANDS["alpha_spmm_alg1"]
),
"spgemm_csr": OperatorTestConfig("spgemm_csr", PERFORMANCE_COMMANDS["spgemm_csr"]),
"sddmm_csr": OperatorTestConfig("sddmm_csr", PERFORMANCE_COMMANDS["sddmm_csr"]),
"spsv_csr": OperatorTestConfig("spsv_csr", PERFORMANCE_COMMANDS["spsv_csr"]),
"spsv_coo": OperatorTestConfig("spsv_coo", PERFORMANCE_COMMANDS["spsv_coo"]),
"spsv_sell": OperatorTestConfig("spsv_sell", PERFORMANCE_COMMANDS["spsv_sell"]),
"spsm_csr": OperatorTestConfig("spsm_csr", PERFORMANCE_COMMANDS["spsm_csr"]),
"spsm_coo": OperatorTestConfig("spsm_coo", PERFORMANCE_COMMANDS["spsm_coo"]),
}
def now_ts() -> str:
return _dt.datetime.now().strftime("%Y%m%d_%H%M%S")
def ensure_dir(path: Path) -> None:
path.mkdir(parents=True, exist_ok=True)
def _capture_text(cmd: list[str], *, cwd: Path, timeout: int = 5) -> str | None:
try:
proc = subprocess.run(
cmd,
cwd=str(cwd),
text=True,
capture_output=True,
timeout=timeout,
check=False,
)
except Exception:
return None
if proc.returncode != 0:
return None
return proc.stdout.strip()
def _module_version(
distribution: str, module_name: str | None = None
) -> dict[str, object]:
info: dict[str, object] = {"installed": False, "version": None}
try:
info["version"] = importlib_metadata.version(distribution)
except importlib_metadata.PackageNotFoundError:
pass
except Exception as exc:
info["version_error"] = str(exc)
try:
module = importlib.import_module(module_name or distribution)
except Exception as exc: