create_ops_docs.py 6.8 KB

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