Files
TheRock/rocm-libraries/dnn-providers/hip-kernel-provider/rocke/style/PYTHON_STYLE.md
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

338 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# rocKE — Python style guide
Conventions for the `rocke` Python code — the platform authoring frontend
(`platform/python/rocke/`) and the `library/` package. These rules
are **derived from the existing code**, so new code is indistinguishable from what is
already here — **except** for typing and other modern-language choices, where the
guide **prescribes the modern standard** (see the PEP baseline below and §3) even when
the current majority is an older form. The intent is that new code converges on modern
Python rather than perpetuating legacy idioms; we do **not** maintain backwards
compatibility with pre-3.10 forms.
This is the human-readable companion to the tooling. Formatting is owned by
**black** (wired into the repo-root `.pre-commit-config.yaml`); linting is done
with **ruff check** (see AGENTS.md — currently run manually, config not yet
committed). Wiring ruff into hooks is deferred; this document defines the rules
regardless of when enforcement lands.
Run locally before pushing (from `platform/`):
```bash
black python/rocke # format
ruff check python/rocke # lint — do NOT pass --fix here (see §9)
# full hook set over your PR range, from the repo root (see AGENTS.md):
pre-commit run --from-ref origin/develop --to-ref HEAD
```
> If a rule here ever conflicts with `AGENTS.md`, `AGENTS.md` wins — it carries the
> hard invariants (byte-identity, relative paths, cross-platform). This guide
> covers *style*; `AGENTS.md` covers *correctness*.
---
## Cardinal rules (never break)
These must never be broken; a violation is always Critical. The confidentiality rules
are owned by `AGENTS.md`; the engineering rules are repeated here because routine Python
edits violate them easily.
- **Compliance** (export controls, restricted data, NPI, product/marketing/code names,
performance data, legal/marketing, internal links) — defined in AGENTS.md
"Compliance", the source of truth. Not restated here.
- **Never autofix emitter code** (`core/`, `helpers/`, `instances/`). The IRBuilder is
side-effecting — `b.const_i32(8)` emits an op even if its handle is unused — so
`ruff --fix` / F841 autofix silently changes kernels. Lint without `--fix`; see §9.
- **Byte-identity.** Any change to emitted IR must be mirrored in the C++ engine and
re-verified with `python tools/check_byte_identity.py`.
- **Relative paths only.** No absolute repo paths, nothing escaping `platform/`; anchor
on `__file__` (enforced by `tests/run_all.py`). See §10.
- **Cross-platform, Python-only scripts.** No `.sh`/shell-only flows; use `pathlib`,
`tempfile`, `shutil.which`. See §10.
## Modern Python baseline (PEPs we encourage)
The package targets **Python ≥ 3.10**. Prefer modern language/typing features; do not
add code that maintains compatibility with pre-3.10 idioms.
- **Style & docs:** PEP 8 (style — black/ruff enforce most), PEP 257 (docstrings),
PEP 20 (the Zen — the tie-breaker for judgment calls).
- **Typing (enforced; see §3):** PEP 585 builtin generics (`list[int]`), PEP 604 unions
(`X | None`), PEP 563 `from __future__ import annotations`, PEP 484/526 (hints &
variable annotations). Prefer PEP 544 Protocols for structural interfaces and
PEP 613 `TypeAlias` for named aliases.
- **Language features:** PEP 557 dataclasses, PEP 498 f-strings, PEP 634–636 structural
pattern matching (`match`/`case`) where it reads better than an `if/elif` chain.
- **Packaging:** PEP 621 (`[project]` metadata), PEP 517/518 (build backend).
- **Not yet — require newer than 3.10; do not use until `requires-python` is bumped:**
PEP 695 (`type` / `class Foo[T]`, 3.12), PEP 692 (`Unpack` kwargs, 3.11),
PEP 698 (`@override`, 3.12).
---
## 1. Formatting (owned by black — do not hand-fight it)
- **Line length 88**, **double quotes**, black defaults. No `[tool.black]` overrides
exist; keep it that way so the repo-wide hook stays authoritative.
- Run `black` before committing; never manually reformat to something black would
undo.
- Use **f-strings** for interpolation (they are the norm across emitters, e.g.
`f" {op.result.name} = call <4 x float> @llvm.amdgcn.mfma..."`). Avoid `%` and
`str.format`.
## 2. Imports
- **`from __future__ import annotations`** at the top of modules is the prevailing
convention — keep using it (it makes annotations lazy strings and lets you
reference types without runtime import cost).
- **Relative imports within the package**, using explicit dot-depth to the target
layer. This is universal here:
```python
from ..ir import Op # core/isa/backend.py
from ...core.arch import ArchTarget # instances/common/mfma_gemm.py
from ...helpers.atoms import MfmaAtom
```
Do **not** use absolute `rocke.core...` imports inside the package, and never a
hardcoded/`sys.path` hack (see AGENTS.md "relative paths only").
- Group imports stdlib → third-party (`numpy`, `torch`) → intra-package, matching
black/ruff-isort ordering. Keep them at module top; local imports inside a
function are reserved for **breaking import cycles** or **optional deps** (e.g.
`from ..core.arch import ArchTarget` inside `is_valid_spec` to avoid a cycle) —
this is an accepted, deliberate pattern, not a default.
## 3. Type annotations — modern only
The package targets **Python ≥ 3.10** and modules carry `from __future__ import
annotations` (PEP 563), so annotations are lazy strings and modern forms cost nothing
at runtime. **Write modern typing; do not maintain the legacy `typing`-module forms.**
- **Annotate every public function signature and dataclass field.** Untyped public
API is out of place in this codebase.
- **Builtin generics (PEP 585), not `typing` aliases:** `list[int]`, `dict[str, int]`,
`tuple[int, ...]`, `set[str]` — **never** `List`, `Dict`, `Tuple`, `Set`.
- **Union with `|` (PEP 604), not `Optional`/`Union`:** `X | None`, `X | Y` —
**never** `Optional[X]` or `Union[X, Y]`.
```python
def is_valid_spec(spec: MfmaGemmSpec, arch: str = "gfx950") -> tuple[bool, str]:
def mfma_k_loop(..., initial_acc: Value | None = None) -> Value:
```
- **`collections.abc`, not `typing`, for ABCs:** `Callable`, `Iterable`, `Sequence`,
`Mapping` come from `collections.abc`.
- **Legacy `typing.Optional/List/Dict/Tuple/Union/Set` is deprecated here.** The bulk
of existing code predates this policy and still uses the capitalized forms; do **not**
add new ones, and modernize opportunistically when you touch a file (don't do
unrelated mass rewrites in a feature change).
- This is enforced by `ruff` pyupgrade rules `UP006` (PEP 585), `UP007`/`UP045`
(PEP 604), `UP035` (deprecated `typing` imports) once the ruff config lands; until
then it is a manual rule.
- Use the domain types (`Value`, `VectorType`, `PtrType`, `KernelDef`) rather than
bare `Any` when a real type exists. `Any` is fine for genuine polymorphism
(e.g. `mma(self, op: Any, ...)` accepting `MmaOp | str`).
## 4. Naming
| Kind | Convention | Example |
|---|---|---|
| Function / method / variable | `snake_case` | `build_mfma_gemm`, `lane_decode` |
| Class / dataclass | `PascalCase` | `MfmaGemmSpec`, `ISABackend`, `MfmaAtom` |
| Module-private helper | `_leading_underscore` | `_binop`, `_operand`, `_mma_c_frag_len` |
| Module-private constant | `_UPPER_SNAKE` | `_SUPPORTED_DTYPES`, `_CATALOG_DTYPE` |
| Public constant | `UPPER_SNAKE` | `F16`, `I32`, `BF16` |
Follow the established **verb prefixes** — they are load-bearing signals of a
function's role, and readers rely on them:
- `build_*` → constructs and returns a `KernelDef` (`build_mfma_gemm`).
- `is_valid_spec` / `validate_*` → validation; see §7 for the return contract.
- `lower_*` / `_op_<opname>` → lowering. Op handlers are named
`_op_<op.name with '.'→'_'>` because dispatch is reflection:
`getattr(self, f"_op_{op.name.replace('.', '_')}")` (`core/lower_llvm.py`). If you
add an op named `tile.foo`, its handler **must** be `_op_tile_foo`.
- `emit_*` → writes IR/text for one construct (`ISABackend.emit_mma`, `atom.emit`).
- Factory `@classmethod`s return a configured instance and are named for the shape
they produce (`MfmaAtom.f16_16x16x16()`), not `from_*`/`make_*` — match the
neighbours in the same class.
### Variable naming (kernel-domain axes)
Name kernel-authoring locals after the **problem's own domain axes**, not generic
`M`/`N`/`K` — those are reserved for the MMA atom shape (`MMA_*`). `TILE_*` and `WAVE_*`
tile the **problem axes**, not `M`/`N`/`K`.
The example column below uses an **attention / FMHA** kernel, whose axes are query
tokens (`Q`), key/value tokens (`KV`), and head dimension (`D`). A different problem
(GEMM, conv, …) uses its own axis names following the same pattern.
| Prefix | Level | Meaning | Example (FMHA) |
|---|---|---|---|
| `Q_` / `KV_` / `D_` | problem axis | the problem's own axes; never `M`/`N`/`K` | `Q`, `KV`, `D` |
| `*_LEN` | tensor | full baked extent of an axis | `Q_LEN`, `KV_LEN`, `D_LEN` |
| `TILE_*` | CTA | per-CTA tile size **along a problem axis** | `TILE_Q`, `TILE_KV` |
| `WAVE_*` | wave | per-wave extent **along a problem axis** | `WAVE_Q`, `WAVE_KV` |
| `MMA_*` | hardware | the fixed MMA atom shape — the **only** place `M`/`N`/`K` appear | `MMA_M`, `MMA_N`, `MMA_K` |
| `N_*` | count | integer count of things | `N_WAVES`, `N_KV_TILES` |
| `*_MMAS` | count | number of MMA output subtiles covering an axis | `Q_MMAS` |
| `<GEMM>_STEPS` | count | number of MMA contraction steps in a named GEMM | `QK_STEPS`, `PV_STEPS` |
| lowercase | runtime | per-lane/per-wave SSA locals | `lane`, `wave` |
Head dimension: the axis is `D` and its full extent is `D_LEN`; `HEAD_D` is a common
synonym — pick one per kernel and keep it consistent.
## 5. Docstrings
Docstrings here are **substantive and explain the WHY**, not just the signature.
This is a deliberate house style for a codebase full of hardware subtleties.
- **Module docstring** on every non-trivial module: what it is and how it fits the
`Spec → build_* → KernelDef → lower → .ll` pipeline.
- **Public functions, classes, dataclasses, and op handlers get docstrings.** State
the contract, the units, and any hardware invariant a caller could get wrong.
- **Encode rationale and provenance.** The codebase routinely cites *why* a value is
what it is — lane layouts, register widths, and correctness traps:
```python
"""K-packed f16 atom on gfx950+. K=32/atom in two halves.
A lane `c4 = lane / 16` holds K = [c4 * 8 : c4 * 8 + 8]
(NOT the flat-concat layout ...; the wrong packing compiles, runs, and
validates within 1e-2 but fails at 1e-3)."""
```
- **Reference tags are allowed** where they add provenance a reader can actually
resolve: runbook sections (`dsl_docs/optimization/runbook_compliance.md`) and
change/phase IDs like `P53`/`P88` (rocke's internal work-item tags). Only use a tag
if a reader can trace it; keep tags in the docstring/comment, not in identifiers.
- Trivial private one-liners may have a single-line docstring or none — don't pad.
## 6. Comments
- Default to **no comment**; add one only when the *why* is non-obvious (a hardware
constraint, a subtle invariant, a correctness trap, a deliberate cycle-breaking
import). The existing comments are almost all "why", e.g. the note that
`b.const_i32(8)` emits an op even when its handle is unused.
- Don't describe *what* well-named code already says, and don't reference the
current task/PR in a comment — that belongs in the commit message.
## 7. Dataclasses, specs, and validation
- **`@dataclass(frozen=True)` is the default** for specs and value objects
(`MfmaGemmSpec`, `MfmaAtom`). Immutability is intentional — specs are hashed and
used as cache/dispatch keys.
- Put **derived quantities behind `@property`**, not in `__init__`
(`MfmaGemmSpec.atom`, `.tile_k`, `.block_size`). Keep stored fields minimal and
compute the rest.
- **Validation returns `tuple[bool, str]`** — `(ok, reason)` with a structured,
actionable reason string; builders call it and raise on failure:
```python
ok, why = is_valid_spec(spec, arch=arch)
if not ok:
raise ValueError(f"invalid mfma_gemm spec for {arch}: {why}")
```
Raise `ValueError` for bad user/spec input and `NotImplementedError` for an
unsupported-but-valid path (e.g. an atom with no dispatch). Error messages name
the offending values and the fix, matching the existing structured style.
## 8. Public API surface (`__all__`)
- Modules that expose a public surface define **`__all__`** (used in ~90 modules).
If you add a public function/class to such a module, add it to `__all__`; keep
genuinely internal helpers out of it (and `_`-prefixed).
- `__init__.py` re-exports the layer's public names; wire new public API through the
nearest package `__init__.py` (e.g. a new builder in `instances/common/` is exported
from `instances/common/__init__.py`) rather than expecting callers to reach into
submodules. Reusable kernels must also be wired into the registry/tests (AGENTS.md).
## 9. Emitter rules (core / helpers / instances) — read before linting
The IR builder is **side-effecting**: calling `b.const_i32(8)` appends an op to the
kernel even if you discard the returned handle. This changes how you lint and edit
emitter code:
- **Never run `ruff check --fix` (or any autofix) on `core/`, `helpers/`,
`instances/`.** F841 "unused local" autofix will delete a binding whose *call*
was the point, silently changing the emitted kernel. Lint with `ruff check`
(no `--fix`) and fix findings by hand. (This is a hard rule from `AGENTS.md`.)
- When a bound value is intentionally unused (ABI params, ordering ops), mark it
explicitly the way the code already does — `# noqa: F841 - ABI` — rather than
removing it:
```python
_M = b.param("M", I32) # noqa: F841 - ABI
```
- **Byte-identity**: any change to emitted IR must be mirrored in the C++ engine in
the same change and re-blessed against the gate. A "style" cleanup that alters
emission is not a style change. When in doubt, run the gate from `platform/` (a built
C++ engine is required first — see AGENTS.md "Build / test / run"):
`python tools/check_byte_identity.py`.
## 10. Cross-platform & paths
- Python only — **no `.sh`/bash helper scripts** under `platform/`. Use `pathlib`,
`tempfile`, `shutil.which`, `os.cpu_count()`; never hardcode `/tmp`, `nproc`,
`sudo`, or shell-only flows.
- **No absolute repo paths and nothing escaping `platform/`.** Anchor on `__file__`.
`tests/run_all.py` enforces this with a grep guard.
## 11. Tests
Test-file naming and the PR-bot test requirement are owned by AGENTS.md. Style note:
most tests are CPU-only (lowering/serialization/byte-identity); gate GPU-only numeric
tests on real hardware being present rather than faking it.
## 12. A minimal conforming module
A new `instances/` builder that assembles the rules above (body elided):
```python
"""Foo GEMM builder — Spec -> build_* -> KernelDef (host authoring)."""
from __future__ import annotations
from dataclasses import dataclass
from ...core.ir import IRBuilder, KernelDef
from ...helpers.atoms import MfmaAtom
__all__ = ["FooSpec", "build_foo", "is_valid_spec"]
@dataclass(frozen=True)
class FooSpec:
M: int
N: int
dtype: str = "f16"
@property
def atom(self) -> MfmaAtom:
return MfmaAtom.f16_16x16x16()
def is_valid_spec(spec: FooSpec, arch: str = "gfx950") -> tuple[bool, str]:
if spec.M % spec.atom.m:
return False, f"M={spec.M} must be a multiple of atom.m={spec.atom.m}"
return True, "ok"
def build_foo(spec: FooSpec, arch: str = "gfx950") -> KernelDef:
ok, why = is_valid_spec(spec, arch=arch)
if not ok:
raise ValueError(f"invalid foo spec for {arch}: {why}")
b = IRBuilder(f"foo_{spec.M}x{spec.N}_{spec.dtype}")
# ... emit body via helpers (loads, mfma_k_loop, epilogue) ...
return b.kernel # the KernelDef
```
Then export `FooSpec`/`build_foo` from `instances/common/__init__.py` and add a
`test_foo.py`. Verify against the current API before copying — this is a shape guide,
not a working kernel.
---
## Quick checklist for a new Python change
- [ ] `black` clean (88 cols, double quotes, f-strings)
- [ ] `from __future__ import annotations`; relative intra-package imports
- [ ] Modern typing only — `list`/`dict`/`tuple`/`X | None`
- [ ] Public signatures typed; docstrings explain the *why* + any hw invariant
- [ ] Verb-prefix naming (`build_*`/`is_valid_*`/`lower_*`/`emit_*`/`_op_<name>`)
- [ ] Specs are `frozen=True`; derived fields via `@property`
- [ ] Validation returns `(ok, reason)`; raise `ValueError`/`NotImplementedError`
- [ ] Public names added to `__all__` / `__init__.py`
- [ ] Emitter code: **no autofix**; `# noqa: F841 - ABI` for intentional unused ops
- [ ] Emission changed? mirror in C++ + re-run the byte-identity gate
- [ ] No absolute paths, no shell scripts