run.py 20.8 KB
Newer Older
1
2
3
4
5
import os
import sys
import subprocess
import argparse
from pathlib import Path
6
from typing import Dict, Tuple, List
7
8


wooway777's avatar
wooway777 committed
9
def find_ops_directory(location=None):
10
    """
wooway777's avatar
wooway777 committed
11
    Find the ops directory by searching from location upwards.
12
13

    Args:
wooway777's avatar
wooway777 committed
14
        location: Starting directory for search (default: current file's parent)
15
16
17

    Returns:
        Path: Path to ops directory or None if not found
18
    """
wooway777's avatar
wooway777 committed
19
20
21
22
23
24
    if location is None:
        location = Path(__file__).parent / "ops"

    ops_dir = location.resolve()
    if ops_dir.exists() and any(ops_dir.glob("*.py")):
        return ops_dir
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

    return None


def get_available_operators(ops_dir):
    """
    Get list of available operators from ops directory.

    Args:
        ops_dir: Path to ops directory

    Returns:
        List of operator names
    """
    if not ops_dir or not ops_dir.exists():
        return []

    test_files = list(ops_dir.glob("*.py"))
    current_script = Path(__file__).name
    test_files = [f for f in test_files if f.name != current_script]

    operators = []
    for test_file in test_files:
        try:
            with open(test_file, "r", encoding="utf-8") as f:
                content = f.read()
                if "infinicore" in content and (
                    "BaseOperatorTest" in content or "GenericTestRunner" in content
                ):
                    operators.append(test_file.stem)
        except:
            continue

    return sorted(operators)
59
60


61
def run_all_op_tests(ops_dir=None, specific_ops=None, extra_args=None):
62
63
64
65
    """
    Run all operator test scripts in the ops directory.

    Args:
66
67
        ops_dir (str, optional): Path to the ops directory. If None, uses auto-detection.
        specific_ops (list, optional): List of specific operator names to test.
68
69
70
        extra_args (list, optional): Extra command line arguments to pass to test scripts.

    Returns:
71
        dict: Results dictionary with test names as keys and (success, return_code, stdout, stderr) as values.
72
73
74
75
76
77
    """
    if ops_dir is None:
        ops_dir = find_ops_directory()
    else:
        ops_dir = Path(ops_dir)

78
    if not ops_dir or not ops_dir.exists():
79
80
81
82
83
        print(f"Error: Ops directory '{ops_dir}' does not exist.")
        return {}

    print(f"Looking for test files in: {ops_dir}")

84
    # Find all Python test files
85
86
87
88
89
90
    test_files = list(ops_dir.glob("*.py"))

    # Filter out this script itself and non-operator test files
    current_script = Path(__file__).name
    test_files = [f for f in test_files if f.name != current_script]

91
    # Filter to include only files that look like operator tests
92
93
94
95
96
    operator_test_files = []
    for test_file in test_files:
        try:
            with open(test_file, "r", encoding="utf-8") as f:
                content = f.read()
97
98
99
100
                # Look for characteristic patterns of operator tests
                if "infinicore" in content and (
                    "BaseOperatorTest" in content or "GenericTestRunner" in content
                ):
101
102
103
104
                    operator_test_files.append(test_file)
        except Exception as e:
            continue

105
    # Filter for specific operators if requested
106
107
108
109
    if specific_ops:
        filtered_files = []
        for test_file in operator_test_files:
            test_name = test_file.stem.lower()
wooway777's avatar
wooway777 committed
110
            if any(op.lower() == test_name for op in specific_ops):
111
112
113
114
115
116
117
118
119
120
121
122
123
124
                filtered_files.append(test_file)
        operator_test_files = filtered_files

    if not operator_test_files:
        print(f"No operator test files found in {ops_dir}")
        print(f"Available Python files: {[f.name for f in test_files]}")
        return {}

    print(f"Found {len(operator_test_files)} operator test files:")
    for test_file in operator_test_files:
        print(f"  - {test_file.name}")

    results = {}

125
126
127
    # Check if verbose mode is enabled
    verbose_mode = extra_args and "--verbose" in extra_args

128
129
130
131
132
133
134
135
    # Check if bench mode is enabled for cumulative timing
    bench_mode = extra_args and "--bench" in extra_args
    cumulative_timing = {
        "total_torch_time": 0.0,
        "total_infinicore_time": 0.0,
        "operators_tested": 0,
    }

136
137
138
139
    for test_file in operator_test_files:
        test_name = test_file.stem

        try:
wooway777's avatar
wooway777 committed
140
141
            # Run the test script - use the absolute path and run from current directory
            cmd = [sys.executable, str(test_file.absolute())]
142
143
144
145
146

            # Add extra arguments if provided
            if extra_args:
                cmd.extend(extra_args)

147
148
            result = subprocess.run(
                cmd,
149
150
                capture_output=True,  # Capture output to analyze
                text=True,
151
            )
152

153
154
155
156
157
            # Analyze output to determine test status
            stdout_lower = result.stdout.lower()
            stderr_lower = result.stderr.lower()

            # Check for operator not implemented patterns
wooway777's avatar
wooway777 committed
158
159
160
161
162
163
164
165
166
167
            if (
                "all tests passed!" in stdout_lower
                and "success rate: 100.0%" in stdout_lower
            ):
                success = True
                returncode = 0
            elif "both operators not implemented" in stdout_lower:
                # Both operators not implemented - skipped test
                success = False  # Not a failure, but skipped
                returncode = -2  # Special code for skipped
168
            elif "operator not implemented" in stdout_lower:
wooway777's avatar
wooway777 committed
169
170
171
                # One operator not implemented - partial test
                success = False  # Not fully successful
                returncode = -3  # Special code for partial
172
            else:
wooway777's avatar
wooway777 committed
173
174
                success = False
                returncode = -1
175

176
177
            results[test_name] = (
                success,
178
179
180
                returncode,
                result.stdout,
                result.stderr,
181
182
183
            )

            # Print the output from the test script
184
185
186
187
            print(f"\n{'='*60}")
            print(f"TEST: {test_name}")
            print(f"{'='*60}")

188
            if result.stdout:
189
                print(result.stdout.rstrip())
190
191

            if result.stderr:
192
193
                print("\nSTDERR:")
                print(result.stderr.rstrip())
194

195
196
197
            # Enhanced status display
            if returncode == -2:
                status_icon = "⏭️"
wooway777's avatar
wooway777 committed
198
                status_text = "SKIPPED"
199
200
            elif returncode == -3:
                status_icon = "⚠️"
wooway777's avatar
wooway777 committed
201
                status_text = "PARTIAL"
202
203
204
205
206
207
208
            elif success:
                status_icon = "✅"
                status_text = "PASSED"
            else:
                status_icon = "❌"
                status_text = "FAILED"

209
            print(
wooway777's avatar
wooway777 committed
210
                f"{status_icon}  {test_name}: {status_text} (return code: {returncode})"
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
            # Extract benchmark timing if in bench mode
            if bench_mode and success:
                # Look for benchmark summary in stdout
                lines = result.stdout.split("\n")
                torch_time = 0.0
                infini_time = 0.0

                for line in lines:
                    if "PyTorch Total Time:" in line:
                        try:
                            # Extract time value (e.g., "PyTorch Total Time: 123.456 ms")
                            torch_time = (
                                float(line.split(":")[1].strip().split()[0]) / 1000.0
                            )  # Convert to seconds
                        except:
                            pass
                    elif "InfiniCore Total Time:" in line:
                        try:
                            infini_time = (
                                float(line.split(":")[1].strip().split()[0]) / 1000.0
                            )  # Convert to seconds
                        except:
                            pass

                cumulative_timing["total_torch_time"] += torch_time
                cumulative_timing["total_infinicore_time"] += infini_time
                cumulative_timing["operators_tested"] += 1

241
242
243
244
            # In verbose mode, stop execution on first failure
            if verbose_mode and not success and returncode not in [-2, -3]:
                break

245
        except Exception as e:
246
            print(f"💥 {test_name}: ERROR - {str(e)}")
247
248
            results[test_name] = (False, -1, "", str(e))

249
250
251
252
253
254
255
256
257
            # In verbose mode, stop execution on any exception
            if verbose_mode:
                print(f"\n{'!'*60}")
                print(
                    f"VERBOSE MODE: Stopping execution due to exception in {test_name}"
                )
                print(f"{'!'*60}")
                break

258
    return results, cumulative_timing
259
260


261
262
263
264
def print_summary(
    results, verbose_mode=False, total_expected_tests=0, cumulative_timing=None
):
    """Print a comprehensive summary of test results including benchmark data."""
265
    print(f"\n{'='*80}")
wooway777's avatar
wooway777 committed
266
    print("CUMULATIVE TEST SUMMARY")
267
268
269
270
    print(f"{'='*80}")

    if not results:
        print("No tests were run.")
271
        return False
272

273
274
275
276
277
    # Count different types of results
    passed = 0
    failed = 0
    skipped = 0
    partial = 0
wooway777's avatar
wooway777 committed
278
279
280
281
    passed_operators = []  # Store passed operator names
    failed_operators = []  # Store failed operator names
    skipped_operators = []  # Store skipped operator names
    partial_operators = []  # Store partial operator names
282
283
284
285

    for test_name, (success, returncode, stdout, stderr) in results.items():
        if success:
            passed += 1
wooway777's avatar
wooway777 committed
286
            passed_operators.append(test_name)
287
288
        elif returncode == -2:  # Special code for skipped tests
            skipped += 1
wooway777's avatar
wooway777 committed
289
            skipped_operators.append(test_name)
290
291
        elif returncode == -3:  # Special code for partial tests
            partial += 1
wooway777's avatar
wooway777 committed
292
            partial_operators.append(test_name)
293
294
        else:
            failed += 1
wooway777's avatar
wooway777 committed
295
            failed_operators.append(test_name)
296

297
298
    total = len(results)

299
300
301
302
303
    print(f"Total tests run: {total}")
    if total_expected_tests > 0 and total < total_expected_tests:
        print(f"Total tests expected: {total_expected_tests}")
        print(f"Tests not executed: {total_expected_tests - total}")

304
    print(f"Passed: {passed}")
305
    print(f"Failed: {failed}")
306

307
    if skipped > 0:
wooway777's avatar
wooway777 committed
308
        print(f"Skipped: {skipped}")
309

310
    if partial > 0:
wooway777's avatar
wooway777 committed
311
312
        print(f"Partial: {partial}")

313
314
315
316
317
318
319
320
321
322
323
324
325
    # Print benchmark summary if cumulative_timing data is available
    if cumulative_timing and cumulative_timing["operators_tested"] > 0:
        print(f"{'-'*40}")
        print("BENCHMARK SUMMARY:")
        print(f"  Operators Tested: {cumulative_timing['operators_tested']}")
        print(
            f"  Total PyTorch Time: {cumulative_timing['total_torch_time'] * 1000:.3f} ms"
        )
        print(
            f"  Total InfiniCore Time: {cumulative_timing['total_infinicore_time'] * 1000:.3f} ms"
        )
        print(f"{'-'*40}")

wooway777's avatar
wooway777 committed
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
    # Display passed operators
    if passed_operators:
        print(f"\n✅ PASSED OPERATORS ({len(passed_operators)}):")
        # Display operators in groups of 10 per line
        for i in range(0, len(passed_operators), 10):
            line_ops = passed_operators[i : i + 10]
            print("  " + ", ".join(line_ops))
    else:
        print(f"\n✅ PASSED OPERATORS: None")

    # Display failed operators (if any)
    if failed_operators:
        print(f"\n❌ FAILED OPERATORS ({len(failed_operators)}):")
        for i in range(0, len(failed_operators), 10):
            line_ops = failed_operators[i : i + 10]
            print("  " + ", ".join(line_ops))

    # Display skipped operators (if any)
    if skipped_operators:
        print(f"\n⏭️ SKIPPED OPERATORS ({len(skipped_operators)}):")
        for i in range(0, len(skipped_operators), 10):
            line_ops = skipped_operators[i : i + 10]
            print("  " + ", ".join(line_ops))

    # Display partial operators (if any)
    if partial_operators:
        print(f"\n⚠️  PARTIAL OPERATORS ({len(partial_operators)}):")
        for i in range(0, len(partial_operators), 10):
            line_ops = partial_operators[i : i + 10]
            print("  " + ", ".join(line_ops))
356
357

    if total > 0:
358
        # Calculate success rate based on actual executed tests
359
360
361
        executed_tests = passed + failed + partial
        if executed_tests > 0:
            success_rate = passed / executed_tests * 100
wooway777's avatar
wooway777 committed
362
            print(f"\nSuccess rate: {success_rate:.1f}%")
363

364
365
366
367
    if verbose_mode and total < total_expected_tests:
        print(f"\n💡 Verbose mode: Execution stopped after first failure")
        print(f"   {total_expected_tests - total} tests were not executed")

368
369
    if failed == 0:
        if skipped > 0 or partial > 0:
wooway777's avatar
wooway777 committed
370
            print(f"\n⚠️  Tests completed with some operators not implemented")
371
372
373
374
            print(f"   - {skipped} tests skipped (both operators not implemented)")
            print(f"   - {partial} tests partial (one operator not implemented)")
        else:
            print(f"\n🎉 All tests passed!")
375
376
        return True
    else:
wooway777's avatar
wooway777 committed
377
        print(f"\n{failed} tests failed")
378
379
380
381
382
383
384
        return False


def list_available_tests(ops_dir=None):
    """List all available operator test files."""
    if ops_dir is None:
        ops_dir = find_ops_directory()
385
    else:
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
        ops_dir = Path(ops_dir)

    if not ops_dir or not ops_dir.exists():
        print(f"Error: Ops directory '{ops_dir}' does not exist.")
        return

    operators = get_available_operators(ops_dir)

    if operators:
        print(f"Available operator test files in {ops_dir}:")
        for operator in operators:
            print(f"  - {operator}")
        print(f"\nTotal: {len(operators)} operators")
    else:
        print(f"No operator test files found in {ops_dir}")
        # Show available Python files for debugging
        test_files = list(ops_dir.glob("*.py"))
        current_script = Path(__file__).name
        test_files = [f for f in test_files if f.name != current_script]
        if test_files:
            print(f"Available Python files: {[f.name for f in test_files]}")


def generate_help_epilog(ops_dir):
    """
    Generate dynamic help epilog with available operators and hardware platforms.

    Args:
        ops_dir: Path to ops directory

    Returns:
        str: Formatted help text
    """
    # Get available operators
    operators = get_available_operators(ops_dir)

    # Build epilog text
    epilog_parts = []

    # Examples section
    epilog_parts.append("Examples:")
    epilog_parts.append("  # Run all operator tests on CPU")
    epilog_parts.append("  python run.py --cpu")
    epilog_parts.append("")
430
431
    epilog_parts.append("  # Run specific operators")
    epilog_parts.append("  python run.py --ops add matmul --nvidia")
432
433
434
435
    epilog_parts.append("")
    epilog_parts.append("  # Run with debug mode on multiple devices")
    epilog_parts.append("  python run.py --cpu --nvidia --debug")
    epilog_parts.append("")
436
437
438
439
440
    epilog_parts.append(
        "  # Run with verbose mode to stop on first error with full traceback"
    )
    epilog_parts.append("  python run.py --cpu --nvidia --verbose")
    epilog_parts.append("")
441
442
443
    epilog_parts.append("  # Run with benchmarking to get cumulative timing")
    epilog_parts.append("  python run.py --cpu --bench")
    epilog_parts.append("")
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
    epilog_parts.append("  # List available tests without running")
    epilog_parts.append("  python run.py --list")
    epilog_parts.append("")

    # Available operators section
    if operators:
        epilog_parts.append("Available Operators:")
        # Group operators for better display
        operators_per_line = 4
        for i in range(0, len(operators), operators_per_line):
            line_ops = operators[i : i + operators_per_line]
            epilog_parts.append(f"  {', '.join(line_ops)}")
        epilog_parts.append("")
    else:
        epilog_parts.append("Available Operators: (none detected)")
        epilog_parts.append("")

    # Additional notes
    epilog_parts.append("Note:")
    epilog_parts.append(
        "  - Use '--' to pass additional arguments to individual test scripts"
    )
    epilog_parts.append(
        "  - Operators are automatically discovered from the ops directory"
    )
469
    epilog_parts.append(
470
        "  - --bench mode now shows cumulative timing across all operators"
471
    )
472
473
474
475
476
477
    epilog_parts.append(
        "  - --verbose mode stops execution on first error and shows full traceback"
    )
    epilog_parts.append(
        "  - In verbose mode, subsequent tests are skipped after first failure"
    )
478
479

    return "\n".join(epilog_parts)
480
481
482


def main():
483
484
485
486
    """Main entry point with comprehensive command line argument parsing."""
    # First, find ops directory for dynamic help generation
    ops_dir = find_ops_directory()

487
    parser = argparse.ArgumentParser(
488
489
490
        description="Run InfiniCore operator tests across multiple hardware platforms",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=generate_help_epilog(ops_dir),
491
492
    )

493
    # Core options
494
495
496
497
498
499
500
501
502
503
504
    parser.add_argument(
        "--ops-dir", type=str, help="Path to the ops directory (default: auto-detect)"
    )
    parser.add_argument(
        "--ops", nargs="+", help="Run specific operators only (e.g., --ops add matmul)"
    )
    parser.add_argument(
        "--list",
        action="store_true",
        help="List all available test files without running them",
    )
505
506
507
508
509
    parser.add_argument(
        "--verbose",
        action="store_true",
        help="Enable verbose mode to stop on first error with full traceback (passed to individual tests)",
    )
510
511
512

    from framework import get_hardware_args_group

wooway777's avatar
wooway777 committed
513
514
    if "-h" in sys.argv or "--help" in sys.argv:
        get_hardware_args_group(parser)
515
516
517

    # Parse known args first, leave the rest for the test scripts
    args, unknown_args = parser.parse_known_args()
wooway777's avatar
wooway777 committed
518
    get_hardware_args_group(parser)
519

520
521
522
    # Handle list command
    if args.list:
        list_available_tests(args.ops_dir)
523
524
525
526
527
        return

    # Auto-detect ops directory if not provided
    if args.ops_dir is None:
        ops_dir = find_ops_directory()
528
529
530
531
532
        if not ops_dir:
            print(
                "Error: Could not auto-detect ops directory. Please specify with --ops-dir"
            )
            sys.exit(1)
533
534
    else:
        ops_dir = Path(args.ops_dir)
535
536
537
        if not ops_dir.exists():
            print(f"Error: Ops directory '{ops_dir}' does not exist.")
            sys.exit(1)
538

539
540
541
542
    # Add verbose flag to extra arguments if specified
    if args.verbose and "--verbose" not in unknown_args:
        unknown_args.append("--verbose")

543
544
545
546
    # Show what extra arguments will be passed
    if unknown_args:
        print(f"Passing extra arguments to test scripts: {unknown_args}")

547
548
549
550
551
552
553
    # Get available operators for display
    available_operators = get_available_operators(ops_dir)

    print(f"InfiniCore Operator Test Runner")
    print(f"Operating directory: {ops_dir}")
    print(f"Available operators: {len(available_operators)}")

554
555
556
    if args.verbose:
        print(f"Verbose mode: ENABLED (will stop on first error with full traceback)")

557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
    if args.ops:
        # Validate requested operators
        valid_ops = []
        invalid_ops = []
        for op in args.ops:
            if op in available_operators:
                valid_ops.append(op)
            else:
                invalid_ops.append(op)

        if invalid_ops:
            print(f"Warning: Unknown operators: {', '.join(invalid_ops)}")
            print(f"Available operators: {', '.join(available_operators)}")

        if valid_ops:
            print(f"Testing operators: {', '.join(valid_ops)}")
573
            total_expected_tests = len(valid_ops)
574
575
        else:
            print("No valid operators specified. Running all available tests.")
576
            total_expected_tests = len(available_operators)
577
578
    else:
        print("Testing all available operators")
579
        total_expected_tests = len(available_operators)
580
581
582

    print()

583
    # Run all tests
584
    results, cumulative_timing = run_all_op_tests(
585
586
587
588
589
        ops_dir=ops_dir,
        specific_ops=args.ops,
        extra_args=unknown_args,
    )

590
    # Print summary and exit with appropriate code
591
592
593
    all_passed = print_summary(
        results, args.verbose, total_expected_tests, cumulative_timing
    )
594
595
596
597
598
599
600

    # Check if there were any tests with missing implementations
    has_missing_implementations = any(
        returncode in [-2, -3] for _, (_, returncode, _, _) in results.items()
    )

    if all_passed and has_missing_implementations:
wooway777's avatar
wooway777 committed
601
        print(f"\n⚠️  Note: Some operators are not fully implemented")
602
603
        print(f"   Run individual tests for details on missing implementations")

604
605
606
607
608
609
610
611
612
613
614
615
    if args.verbose and not all_passed:
        print(
            f"\n💡 Verbose mode tip: Use individual test commands for detailed debugging:"
        )
        failed_ops = [
            name
            for name, (success, _, _, _) in results.items()
            if not success and name in results
        ]
        for op in failed_ops[:3]:  # Show first 3 failed operators
            print(f"   python {ops_dir / (op + '.py')} --verbose")

616
    sys.exit(0 if all_passed else 1)
617
618
619
620


if __name__ == "__main__":
    main()