1#!/usr/bin/env python3
  2
  3"""
  4This script parses docs/ops/*.csv and creates the ops.md, which is a table documenting supported operations on various ggml backends.
  5"""
  6import csv
  7import logging
  8import sys
  9from pathlib import Path
 10from collections import defaultdict
 11
 12
 13class DocsGenerator:
 14    def __init__(self, ggml_root: str, output_filename: str = "ops.md"):
 15        self.ggml_root = Path(ggml_root)
 16        self.ops_dir = self.ggml_root / "docs" / "ops"
 17        self.output_filename = output_filename
 18        self.backend_support: dict[str, dict[str, list[bool]]] = defaultdict(
 19            lambda: defaultdict(list)
 20        )
 21        self.all_operations: set[str] = set()
 22        self.all_backends: set[str] = set()
 23        self.logger = logging.getLogger(__name__)
 24
 25    def parse_support_files(self) -> None:
 26        if not self.ops_dir.exists():
 27            self.logger.warning(f"ops directory not found: {self.ops_dir}")
 28            return
 29
 30        self.logger.info(f"Parsing support files from {self.ops_dir}...")
 31
 32        for support_file in self.ops_dir.glob("*.csv"):
 33            self.logger.info(f"  Reading: {support_file.name}")
 34            self._parse_support_file(support_file)
 35
 36    def _parse_support_file(self, file_path: Path) -> None:
 37        try:
 38            with open(file_path, "r", newline='') as f:
 39                reader = csv.DictReader(f)
 40
 41                for row in reader:
 42                    # Skip rows that don't have support mode
 43                    if row.get('test_mode') != 'support':
 44                        continue
 45
 46                    backend_name = row.get('backend_name', '').strip()
 47                    operation = row.get('op_name', '').strip()
 48                    supported_str = row.get('error_message', '').strip()  # "yes" or "no"
 49                    backend_reg_name = row.get('backend_reg_name', '').strip()
 50
 51                    # Skip invalid or error operations
 52                    if not operation or not backend_name or operation in [
 53                        "CONTEXT_ERROR",
 54                        "BUILD_ERROR",
 55                    ]:
 56                        continue
 57
 58                    is_supported = supported_str.lower() == "yes"
 59
 60                    # Use backend_reg_name for grouping, fallback to backend_name
 61                    backend_key = backend_reg_name if backend_reg_name else backend_name
 62
 63                    self.all_backends.add(backend_key)
 64                    self.backend_support[backend_key][operation].append(is_supported)
 65                    self.all_operations.add(operation)
 66
 67        except Exception as e:
 68            self.logger.error(f"    Error parsing {file_path}: {e}")
 69
 70    def get_backend_support_status(self, backend: str, operation: str) -> str:
 71        support_list = self.backend_support[backend].get(operation, [])
 72
 73        if not support_list:
 74            return "unsupported"
 75
 76        all_supported = all(support_list)
 77        any_supported = any(support_list)
 78
 79        if all_supported:
 80            return "supported"
 81        elif any_supported:
 82            return "partially supported"
 83        else:
 84            return "unsupported"
 85
 86    def get_support_status(self, operation: str) -> str:
 87        if operation not in self.all_operations:
 88            return "unsupported"
 89
 90        support_count = 0
 91        total_backends = len(self.all_backends)
 92
 93        for backend in self.all_backends:
 94            if self.backend_support[backend].get(operation, False):
 95                support_count += 1
 96
 97        if support_count == 0:
 98            return "unsupported"
 99        elif support_count == total_backends:
100            return "supported"
101        else:
102            return "partially supported"
103
104    def get_support_symbol(self, status: str) -> str:
105        symbols = {"supported": "โœ…", "partially supported": "๐ŸŸก", "unsupported": "โŒ"}
106        return symbols.get(status, "โ“")
107
108    def generate_markdown(self) -> str:
109        lines = []
110
111        lines.append("# GGML Operations")
112        lines.append("")
113        lines.append("List of GGML operations and backend support status.")
114        lines.append("")
115        lines.append("## How to add a backend to this table:")
116        lines.append("")
117        lines.append("1. Run `test-backend-ops support --output csv` with your backend name and redirect output to a csv file in `docs/ops/` (e.g., `docs/ops/CUDA.csv`)")
118        lines.append("2. Regenerate `/docs/ops.md` via `./scripts/create_ops_docs.py`")
119        lines.append("")
120        lines.append("Legend:")
121        lines.append("- โœ… Fully supported by this backend")
122        lines.append("- ๐ŸŸก Partially supported by this backend")
123        lines.append("- โŒ Not supported by this backend")
124        lines.append("")
125
126        backends = sorted(self.all_backends)
127        header = "| Operation |"
128        for backend in backends:
129            header += f" {backend} |"
130
131        separator = "|-----------|"
132        for _ in backends:
133            separator += "------|"
134
135        lines.append(header)
136        lines.append(separator)
137
138        sorted_operations = sorted(self.all_operations)
139
140        for operation in sorted_operations:
141            row = f"| {operation:>32} |"
142
143            for backend in backends:
144                status = self.get_backend_support_status(backend, operation)
145                if status == "supported":
146                    symbol = "โœ…"
147                elif status == "partially supported":
148                    symbol = "๐ŸŸก"
149                else:
150                    symbol = "โŒ"
151                row += f" {symbol} |"
152
153            lines.append(row)
154
155        lines.append("")
156
157        return "\n".join(lines)
158
159    def run(self) -> None:
160        self.logger.info("Parsing GGML operation support files...")
161        self.parse_support_files()
162
163        if not self.all_operations:
164            self.logger.error(
165                "No operations found. Make sure to run test-backend-ops support --output csv > docs/ops/file.csv first."
166            )
167            return
168
169        self.logger.info(
170            f"Found {len(self.all_operations)} operations across {len(self.all_backends)} backends"
171        )
172
173        self.logger.info("Generating markdown...")
174        markdown_content = self.generate_markdown()
175
176        docs_dir = self.ggml_root / "docs"
177        docs_dir.mkdir(exist_ok=True)
178
179        ops_file = docs_dir / self.output_filename
180        with open(ops_file, "w") as f:
181            f.write(markdown_content)
182
183        self.logger.info(f"Generated: {ops_file}")
184        self.logger.info(f"Operations: {len(self.all_operations)}")
185        self.logger.info(f"Backends: {len(self.all_backends)}")
186
187
188def main():
189    logging.basicConfig(level=logging.INFO)
190
191    if len(sys.argv) > 1:
192        output_filename = sys.argv[1]
193    else:
194        output_filename = "ops.md"
195
196    generator = DocsGenerator(".", output_filename)
197    generator.run()
198
199
200if __name__ == "__main__":
201    main()