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()