Status: current · governs the whole docs/ tree · introduced 2026-09-30 with the Doxygen site.
How the site is built, where each kind of material lives, the anchor contract between code comments and pages, how to write API documentation, and how to record results.
The site is the project's database of results, rationale and design decisions. Every routing window, rejected kernel and measured ratio that the code relies on is written here, and the code points at it.
Style
- Reference-book style: say what a thing is, how to use it, and the numbers that matter.
- Open with the H1 and one status line, then a one-to-three sentence summary.
- Short paragraphs (3 sentences at most), bullets and tables over prose, present tense, active voice, no "we".
- No narrative history ("used to", "phase 3", "we tried"). State the current fact. A deleted feature someone may look for gets one sentence.
- Keep one sentence of "why" only where a reader needs it to avoid breaking a threshold, invariant or trap. Say each fact once and link to it.
- Collapse rejected alternatives into a | Alternative | Result | Verdict | table, numbers kept.
- Use > **Note:** or > **Warning:** blockquotes for genuine traps only.
- Preserve information when you migrate or distil: every measured number, conclusion, open debt and trap. Mark a partly stale claim as stale rather than deleting it.
Building the site
sh scripts/build_docs.sh # -> build/docs/html/index.html
DOXYGEN=/path/to/doxygen sh scripts/build_docs.sh /tmp/site
BATCHLAS_DOCS_STRICT=1 sh scripts/build_docs.sh # warnings are errors, as in CI
scripts/build_docs.sh is the only build path. It checks the Doxygen version, generates the database pages (docs/tools/gen_db_pages.py, see Adding results), runs doxygen docs/Doxyfile from the repository root, then docs/tools/gen_site_assets.py (the navigation tree and the search index, see Theme) and docs/tools/check_doc_anchors.py against the XML output. Warnings go to <out>/doxygen-warnings.log. It needs no SYCL tree or compiler.
| Entry point | What it does |
| sh scripts/build_docs.sh [out] | The build. Default out dir build/docs. |
| -DBATCHLAS_BUILD_DOCS=ON, target batchlas_docs | CMake wrapper (cmake/BatchLASDocs.cmake) that calls the script with DOXYGEN set to the doxygen CMake found, into <build>/docs. cmake --install installs html/ under the doc dir when built. |
| sh .github/ci/run_local_checks.sh | Runs gen_db_pages.py --self-test, then the build into build/docs when Doxygen 1.18+ is on PATH (or in DOXYGEN); prints a skip line otherwise. |
| CI job docs | Installs the pinned Doxygen, runs the script with BATCHLAS_DOCS_STRICT=1, uploads build/docs/html as the docs-html artifact. docs-deploy publishes to GitHub Pages on a push to main. See the CI page. |
Doxygen is pinned to 1.18.0. The script refuses older versions; CI installs the official release tarball and checks its sha256 against ci.yml. The Doxyfile uses MARKDOWN_ID_STYLE = GITHUB, the anchor contract below was verified on 1.18, and docs/theme/header.html is the 1.18 default header plus the theme's scripts. Distribution packages (Ubuntu 22.04 ships 1.9.1) are too old.
Theme. doxygen-awesome-css 2.5.0 is vendored under docs/theme/doxygen-awesome/ with its MIT LICENSE, which must stay beside it. On top of it the site follows Material for MkDocs, with Doxygen's own tree view, outline panel and search turned off (GENERATE_TREEVIEW, PAGE_OUTLINE_PANEL, SEARCHENGINE = NO):
| Piece | Where |
| Top bar (brand, theme toggle, search field, repository) | #titlearea in docs/theme/header.html |
| Layout, colours, light/dark tokens | docs/theme/batchlas.css |
| Section tabs (the top-level @subpage pages), the current section's page list (the whole tree only in the phone drawer), "Table of contents" (the page's own headings), search panel (API hits show the signature) | docs/theme/bl-site.js |
| Navigation tree (bl-nav.js) and full-text index (bl-search.js, one record per section of every page, group and class; members carry their prototype) | generated by docs/tools/gen_site_assets.py from xml/ after Doxygen runs |
| API page shape: description, then the full member documentation (functions first), then any class or sub-group list; no summary lists of functions, enums or variables (ALWAYS_DETAILED_SEC = YES keeps brief-only members) | docs/theme/DoxygenLayout.xml, docs/Doxyfile |
| Prototypes as code blocks (one parameter per line, links kept) | prettyPrototypes() in docs/theme/bl-site.js |
The navigation follows the @subpage tree from the main page, plus the API group tree, so a page appears in it only if some section page lists it with @subpage. gen_site_assets.py also appends ?v=<content hash> to the theme's CSS/JS URLs in every page, so a redeploy is never hidden behind a browser's cached stylesheet.
Bumping either.
- Doxygen: change DOXYGEN_VERSION and DOXYGEN_SHA256 in .github/workflows/ci.yml (hash the release's linux.bin.tar.gz yourself), build locally with BATCHLAS_DOCS_STRICT=1, and re-run python3 docs/tools/check_doc_anchors.py --xml <out>/xml, because heading-id rules change between versions. Regenerate the header with doxygen -w html header.html footer.html style.css and re-apply the doxygen-awesome-*.js and bl-*.js script lines and the #titlearea markup.
- Theme: replace the files in docs/theme/doxygen-awesome/ (including LICENSE) from the release, update the version in the first line of docs/theme/header.html, and compare the theme's recommended header against ours.
Where things go
| Material | Location | Label prefix |
| Measurements, grids, A/B results, rejected alternatives, bracketing evidence for a routing window | docs/perf/<op>.md (evidence pages) | perf_<op> |
| Design decisions, API design records, "why this architecture", known defects | docs/design/<topic>.md | design_<topic> |
| Mathematics and derivations | docs/algorithms/<topic>.md | algo_<topic> |
| User guides | docs/guide/<topic>.md | guide_<topic> |
| Process and tooling (CI, this page, profiling workflows) | docs/developer/<topic>.md | dev_<topic> |
| Raw result files (CSV, logs) | benchmarks/results/ in Git LFS | none; catalogued by the Results database page |
| API contract (what a function does, its parameters, preconditions) | Doxygen comments in include/batchlas/ | @ingroup |
| Invariants, deliberate oddities, traps | a short code comment plus an evidence pointer | none |
- Markdown lives under docs/. Exceptions: a directory README.md (never one at the root), .claude/CLAUDE.md (only imports the agent guide) and files under .github/ (the project README is .github/README.md). .github/ci/check_markdown_locations.py enforces this. No plan files at the root: write the plan's conclusions into the page for its area and keep task lists out of the tree.
- A code comment carries only an invariant, a "looks wrong but is deliberate" note or a trap, plus evidence: docs/<path>.md#<anchor>. The per-file density ceiling (18%, .github/ci/check_comment_density.py) keeps lab-notebook material out of the source.
- A new page starts with # Title {#label} (snake_case label, prefix from the table) and a one-line status; see Page template.
- Math uses \f$ ... \f$ inline and \f[ ... \f] for display. $...$ is not rendered.
- Links: relative Markdown links ([the LU page](../perf/lu.md#<anchor>)) or @ref <label>. A broken link is a warning, and CI fails on warnings.
Anchors are the contract
Several hundred comments (447 pointers on 2026-09-30) in src/, include/, tests/, benchmarks/ and the CMake files cite a heading with evidence: docs/<page>.md#<anchor>. The anchor is the GitHub slug of the heading text (lower case, punctuation dropped, spaces to hyphens), so a heading's wording is an interface.
- Shortening a cited heading. Pin the old slug as an explicit id: ## Shorter title {#exact-old-slug}. The heading may move or shrink to a sentence, but not be deleted while anything else cites it.
- Distinctive text. .github/ci/check_evidence_anchors.py (CI job comment-density) resolves every pointer against the named page and requires every cited slug to be unique across all of docs/. Doxygen numbers a repeated heading site-wide (the second Results becomes results-1), GitHub per page. Write syevx: the bisection crossover, not Results.
- No leading numbers. Doxygen gives ## 11. Foo an autotoc_md-prefixed id, so its GitHub slug is not live on the site (the known-defects page uses ## Defect 11: ...).
- End-to-end check. docs/tools/check_doc_anchors.py, run by build_docs.sh, looks up each cited anchor in Doxygen's XML for the generated section id.
- Before rewording or deleting a section read the generated Evidence index (@ref evidence_index), which maps each cited section to every file and line that depends on it. When you rename a cited heading, update every pointer in the same change.
Writing API documentation
API documentation lives in the public headers (include/batchlas/, include/batchlas.hh) and states the contract: what the function computes, each parameter, the return value, preconditions, what it throws, and complexity or workspace needs that matter to a caller. Implementation rationale belongs on a design or evidence page, linked with @see or an evidence pointer.
- /** ... */ and /*! ... */ are equivalent. JAVADOC_AUTOBRIEF is on, so the first sentence is the brief. Use ///< for a trailing member comment.
- The Doxyfile defines the aliases @invariant, @trap and @evidence{label}.
- Every public entity belongs to a group, defined in docs/pages/api_groups.dox: api_core, api_matrix, api_enums, api_options, api_blas2, api_blas3, api_factorizations, api_qr, api_eigen, api_tridiag, api_svd, api_sparse, api_extra, api_linalg, api_dispatch, api_workspace, api_errors, api_config, api_device_blas, api_internal_helpers, all nested under api_reference.
- Add @ingroup <id> to the declaration, or wrap a run of declarations in @addtogroup <id> ... @{ ... @}. Add a new group to api_groups.dox, not in a header.
Comment-density exemption. In the public headers, comment-only lines of a Doxygen doc comment (///, //!, a block opened by /** or /*!) count in neither numerator nor denominator of the 18% ceiling (check_comment_density.py --all reports them as doc).
- A /*** banner, an empty /**/ and a //// rule are ordinary comments; a line mixing a doc comment and a plain one counts.
- The same doc comments in src/, tests/ or benchmarks/ count normally, because Doxygen does not publish those.
- The exemption covers the contract only. Rationale, measurements and history written with /// are still rationale and review rejects them; move them to docs/ and leave a pointer.
Documenting an op's choices
src/ops/<op>/choice.hh is the op's selection vocabulary (see Kernel selection (developer) and Flat kernel selection). It is on the site (the Doxyfile lists src/ops in INPUT; FILE_PATTERNS matches no .cc) but lives in src/, so its doc comments count toward the 18% ceiling. Trailing ///< comments are scored as code, so the pattern below documents the whole vocabulary in about four comment lines. A new op copies it and needs no page edits: the api_selection_ops group lists whatever files add themselves to it.
- File block, two lines. /// @file, then /// @brief <op>: <family>, <family>, .... evidence: docs/perf/<page>.md @ingroup api_selection_ops. The brief names the families in candidate order, the evidence pointer names the op's docs/perf/ page. Keep the line within 120 columns; drop the anchor before dropping the group.
- Field-less family: one trailing ///< on its struct X : select::NoFields<"x"> {}; line: the driver it runs, then its can_run limits beyond the op's common native term (n <= 32, Lower only, needs the solver library).
- Family with fields (gemm's reg/wide, potrf's lpanel): a preceding /// block giving the driver, what each field means and its legal values. A trailing ///< on struct X { documents the first member, not the struct. A field declared alone gets its own ///<; on a multi-name declaration (int m, n, k;) only the last name gets it, so describe them in the struct block.
- The choice variant: a trailing ///< with the common native term (GPU, sub-group 32,
square, no NETLIB) or, for an op with no vendor family, that a vendor pin warns and runs Auto.
- candidates<T>(): template <class T> /// Every compiled choice, in tie-break order (§6.3). A /// after code on the template line attaches to the function and counts as code. A preceding /// line (potrf, geqrf, gemm) is equivalent for Doxygen and costs one comment line.
- spec, last_resort, key_names, grid_*: a trailing ///< (on the first or last line of a multi-line initialiser) or a short preceding /// block where the file has budget. Key comments say what each key measures and why its :log weight is what it is (work \(\sim n^3\) means weight 3); grid comments say which old thresholds the grid straddles.
- Section numbers §x.y and rule ids R1-R8 refer to docs/design/flat-kernel-selection.md. Cite a section by number with a page-only pointer, evidence: docs/design/flat-kernel-selection.md (§5.4): numbered headings get autotoc_md ids on the site, so a #54-... anchor is not live. Cite any other anchor in a plain comment, or in backticks inside a /// comment: Doxygen reads #anchor in a doc comment as a link request and warns, while the checker's pattern stops at the backtick, so a backticked pointer is still checked.
- Helpers next to the vocabulary (can_run.hh, vendor.hh, sizing helpers such as geqrf.hh) get the same two-line file block, or a function doc ending in @ingroup api_selection_ops.
- <op>.cc is not on the site. Its comments stay plain: a 2-4 line header (the op, R1, which family runs which driver), // Correctness only (R3) above can_run naming what each clause guards, // Exactly the chosen family's need (R5) above the workspace visitor, and invariants at the line they guard. Measurements and history go to the op's docs/perf/ page.
Adding results
- Raw data goes in benchmarks/results/ (Git LFS). Run git lfs install once per machine before committing, or the file is committed raw and .github/ci/check_lfs_pointers.py fails CI. Name it <campaign>_<op>_<dtype>[_<machine>].csv; the generator reads op, type and machine from the name. The only known machine token is rtx4090; another machine needs its token added to MACHINES in docs/tools/gen_db_pages.py, or its row claims the primary box.
- The evidence section that uses the data records, next to the numbers: the machine (GPU and SM, host if it matters), the date, the command or harness with its flags (batch, warm-up, iterations, pinned route) and the raw file path. A ratio without machine and date cannot be re-checked when the vendor library moves. Follow the measurement rules on Routing and performance evidence.
- The Results database page (@ref results_database) is generated on every build from benchmarks/results/: one row per file with campaign, op, type and machine (from the name), the date and commit that added it, whether this checkout holds the data or only the LFS pointer, and which docs/ pages mention the file by name. — in the last column means no page distils the grid yet.
- Older raw evidence is at the tag perf-evidence/vendor-independence (git show perf-evidence/vendor-independence:experiments/<path>); pages cite those paths as archive paths.
Page template
# <op or topic>: <what the page is about> {#perf_<op>}
> **Status:** current · RTX 4090 (sm_89), CUDA 13.2 · measured <dates as recorded in the source>
<One to three sentences: what the page gives the reader and the conclusion needed first.>
## <op>: the shipped predicate
<Quote the predicate with file:line. Read the predicate, not the prose.>
## <op>: <distinctive name of the window or decision>
<Numbers, with the bracketing non-winner on each edge. Raw data:
benchmarks/results/<file>.csv. Command: <exact command>.>
## <op>: rejected alternatives
| Alternative | Result | Verdict |
| --- | --- | --- |
## <op>: open debts
<What is still owed and what would settle it.>
- Status line: current or historical, superseded by @ref <label>., then machine (measurement pages only) and date, separated by ·.
- For a design, algorithm, guide or developer page use the matching label prefix from Where things go and drop the machine and measured parts.