Releasing mmaemu¶
The maintainer prepares and publishes a release. No script here pushes or tags; the only upload to PyPI is
release.yml's publish job, which runs on a v* tag pushed by hand and waits for approval of the pypi environment.
docs.yml deploys the documentation site to GitHub Pages, and only once the repository is public. Steps marked
[by hand] publish something and are done by the maintainer by hand; every other step leaves a committed log.
The environment is the one of testing.md: a venv with torch 2.7.1+cu128, nvcc 12.8.61 for building the
release fatbins, and a GPU lock ($GPU, i.e. $MMAEMU_GPU_LOCK; the commands are for bash; in zsh, which needs
setopt interactivecomments for their trailing comments, $GPU is written ${=GPU}). T2 and the Evo 2 check of T7
need the unpublished hardware ground truth (MMAEMU_TRUTH_DIR), so a release can only be validated on the
maintainer's host.
The wheel is pure Python (py3-none-any). It ships the kernel sources and GPU-validated fatbins built locally with
a pinned nvcc (section 2). GitHub runners have no GPU, so the release workflow never builds kernels: it checks that
the committed fatbins are current and intact and packages exactly those bytes.
1. Before the freeze¶
-
On master:
-
python tools/check_docs.pyexits 0; -
tools/ci_local.shpasses (the CI steps run locally; see section 5).
-
-
CI (
.github/workflows/ci.yml) is green on the full matrix: T0 + T1 on Python 3.10-3.13 x torch 2.6.0, 2.7.1 and the latest torch (CPU). Torch floor: settorch>=inpyproject.tomlto the lowest torch of that matrix whose T0 + T1 pass, and drop the failing matrix entries. -
T2, the full replay of the hardware ground truth, is committed under
docs/validation_logs/<date>/with the mmaemu commit, the truth manifest sha256 and the per-recording comparison with the audit's reference replay: PASS. -
T3 (GPU conformance), T3p (live
torch._scaled_mmparity on the profile device), T4 (mutation matrix) and T6 (performance) are run on the RTX 4060 and their logs committed. After a GPU swap: T3 on the new GPU, and T3p recorded as "profile device unavailable". T3p runs only on the RTX 4060: its tests useada. The H100 PCIe profiles have no live tier; their hardware evidence is the committed probe and split-K sweep logs.tests/test_h100_mini.pyreplays 124 of the probe's live calls (in CI, T0 + T1 on the CPU, and on the GPU in T3),tests/test_splitk_table.pychecks the H100 table, its envelope and the resolver against both logs, and T7 compares one Evo 2 projection with the H100's outputs (section 3); the full replays of the probe and the sweep (tools/replay_h100_log.py) belong to no tier. T3 also runs on an RTX 5070 Ti: v1, v4, h1 and h4 are validated on sm_120 as well as on sm_89. -
docs/validation.mdcites the committed T2/T3/T4 logs (log:references) anddocs/performance.mdthe T6 numbers;python tools/check_docs.pyresolves every reference.
2. Release-candidate freeze (on the maintainer's host)¶
export PYTHONPATH=$PWD PYTHONDONTWRITEBYTECODE=1
python tools/build_fatbin.py --release --out build/fatbin-release # every module; nvcc 12.8.61 (--nvcc / $NVCC / $CUDA_HOME / PATH)
for f in build/fatbin-release/*.fatbin; do cmp "$f" "mmaemu/backends/cuda/bin/${f##*/}"; done # byte for byte
# build_report.json, cuobjdump.txt and the comparison are the build log: keep them under
# docs/validation_logs/<date>/, not in bin/ (check_dist accepts only the manifest and its fatbins there)
# settle bin/ module by module (below), then:
python tools/build_fatbin.py --check --require-manifest # staleness check by source hashes, no nvcc
python tools/check_dist.py --verify-manifest # fatbin sha256 == manifest
tools/run_gpu_tests.sh # T3 on the fatbins and on NVRTC (locks per file): bitwise equal
# it tests the package's bin/ when that has a manifest ($MMAEMU_TEST_FATBIN_DIR overrides), and a fatbin test
# skipped for want of a manifest fails the run
Check the cuobjdump architecture lists against the sass_archs of every KernelSpec, 0 spill bytes and the
forbidden-PTX grep. Do not copy the build's manifest.json into bin/: it records the rebuilt bytes. Settle each
module in mmaemu/backends/cuda/bin/ instead:
- Rebuilt bytes identical to the committed fatbin: the fatbin stays. If its sources changed (a comment, for
example), its manifest entry takes the new
source_sha256. - Rebuilt bytes that differ: compare the SASS of every architecture and the PTX of the two builds before anything is replaced. A rebuilt fatbin that replaces the committed one is copied together with its manifest entry, and T3 runs on it.
At the 0.1.0 freeze h1, h4, v9, tc3 and the probes were rebuilt byte-identical (h1's entry took the source_sha256
of a comment change). v1 and v4 differed only in the hash nvcc puts into the names of anonymous-namespace symbols
in the PTX, with identical SASS on every architecture, so their committed bytes, on which T3 had run, were kept
(rc-fatbin). Commit mmaemu/backends/cuda/bin/ (the
.gitignore does not ignore it) and the build log.
Between freezes, a branch that adds or changes a kernel commits that module's release build:
python tools/build_fatbin.py --release --modules <module> --out <dir> writes a manifest with that module only. Its
fatbin goes to mmaemu/backends/cuda/bin/ and its entry into bin/manifest.json, with the other entries
unchanged. Its build_report.json and cuobjdump.txt go under docs/validation_logs/<date>/. h1
(h1-fatbin) and h4
(h4-fatbin) were committed this way. The release-candidate
freeze then rebuilds such a module with all the others.
3. Version, changelog, distributions¶
-
Set the version in
mmaemu/_version.py(0.1.0),CITATION.cff(version:anddate-released:) and theCHANGELOG.mdheading; entries listed under "Unreleased" go into the version's section. ValidateCITATION.cffwithcffconvert --validate -i CITATION.cff(cffconvert 2.0.0, in a scratch venv): Zenodo reads it at the release tag (section 4, step 8). The changelog lists the scope (for 0.1.0 both reference platforms, the H100 PCIe and the RTX 4060), the maturity of every preset and profile and the limitations. If v9 or tc3 did not pass acceptance at the freeze (they are not release-blocking), remove them from the CHANGELOG, README and docs/performance.md and say they follow in 0.1.x. -
Build both distributions from the tree and check them:
python -m pip install build twine # in a scratch venv python -m build --sdist --wheel # NOT a bare `python -m build`: that builds the wheel from the sdist, which has no fatbins twine check --strict dist/* python tools/check_dist.py dist/* --require-fatbins --tag v0.1.0 -
T7, the release validation of the built wheel on the GPU:
$GPU timeout 600 python tools/validate_release.py dist/mmaemu-0.1.0-py3-none-any.whlIt creates a fresh venv with no nvcc on
PATHand noCUDA_HOME, installs torch and the wheel, and runspython -m mmaemu info,python -m mmaemu selftest --force, a T3 conformance subset (each kernel against the CPU reference of its own preset) and one Evo 2 projection against its hardware recordings: the RTX 4060's underadaand the H100's (sha256 from the probe log) underhopper. It writesdocs/release_logs/0.1.0_<gpu>.json; every check must be PASS. Commit that file. (A clean-venv sdist install on a GPU uses NVRTC;tools/validate_release.pywith the sdist's wheel and--allow-missing-fatbinschecks that path.) -
Licence:
LICENSEis the Apache-2.0 text;NOTICEcarries the BSD-2 notice of the MATLAB Tensor Core toolbox;check_distverifies that both andLICENSES/are in the wheel'sdist-info/licenses/and that the metadata saysLicense-Expression: Apache-2.0. -
Mark the validated development commit with
git tag -f -a rc/v0.1.0 -m "mmaemu 0.1.0 release candidate"(-fmoves the tag when an earlier candidate holds it). Section 4 takes the snapshot tree from this tag: check thatgit rev-parse 'rc/v0.1.0^{commit}'is the validated commit. The release tagv0.1.0goes on the public snapshot commit (section 4), not on the development history. Nothing is pushed before section 4.
4. Publishing [by hand]¶
The public repository carries snapshot trees only, never the development history: each snapshot is one commit whose tree is exactly a validated candidate tree. The first snapshot has no parent; every later one has the previous snapshot as its only parent, so the repository never needs a force push.
Staging. Before the release, astding/mmaemu exists as a private repository that receives the pre-release
snapshots in exactly this way (a parentless first snapshot, then snapshots parented on it). Because it never held
any other commit, it can later be made public as it is; its history is then these snapshots and the release. The
maintainer decides when to switch its visibility. On the GitHub Free plan of astding/mmaemu, environments with
required reviewers, rulesets and Pages are available only in public repositories
(environments,
rulesets),
so they are set up after the switch to public (step 2) and before the tag push (step 5).
-
[by hand] Create the public snapshot commit and its tag in the development clone:
TREE=$(git rev-parse 'rc/v0.1.0^{tree}') export GIT_AUTHOR_NAME=astding GIT_AUTHOR_EMAIL=50975126+astding@users.noreply.github.com export GIT_COMMITTER_NAME=astding GIT_COMMITTER_EMAIL=50975126+astding@users.noreply.github.com PREV=$(git ls-remote https://github.com/astding/mmaemu.git refs/heads/master | cut -f1) # empty for the first PUB=$(git commit-tree "$TREE" ${PREV:+-p} ${PREV:+"$PREV"} -m "mmaemu 0.1.0") # bash and zsh git tag -a v0.1.0 "$PUB" -m "mmaemu 0.1.0" test "$(git rev-parse "${PUB}^{tree}")" = "$TREE" # same tree git rev-list "${PUB}" | wc -l # the previous snapshots + 1; `git log` shows no development commit(
$PREVmust be fetched into the development clone first:git fetch https://github.com/astding/mmaemu.git master.) Never force-push toastding/mmaemuand never push development commits to it: GitHub keeps force-pushed commits reachable by SHA, and a repository that once held other commits must not be made public. Push only these explicit refs, never--all,--mirroror--tags:git push https://github.com/astding/mmaemu.git "${PUB}:refs/heads/master"Pre-release CI runs the same way in the private repository
astding/mmaemu-ci, which also only ever receives snapshots of candidate trees (each parented on the previous one). Never push av*tag to either repository before step 5: it starts the release workflow. -
[by hand] Make
astding/mmaemupublic. Making it public also publishes its Actions history, logs and unexpired artifacts. Before switching, delete the release runs thatworkflow_dispatchstarted on pre-release snapshots: theirdistartifacts are pre-release builds of the same version (Actions → release → … next to the run → Delete workflow run, orgh run delete <run-id> -R astding/mmaemufor each id thatgh run list -R astding/mmaemu -w release -e workflow_dispatchlists). After switching, set Settings → Actions → General → "Approval for running fork pull request workflows from contributors" to Require approval for all external contributors (ci.yml and docs.yml run on pull requests).Documentation site: Settings → Pages → Build and deployment → Source: GitHub Actions, then run the
docsworkflow (Actions → docs → Run workflow) and check https://astding.github.io/mmaemu/. Its build job (mkdocs build --strict, mkdocs.yml) runs on every push and pull request; its deploy job, the only one there withpages: writeandid-token: write, is skipped while the repository is private and deploys only master. -
[by hand] On PyPI, add a trusted publisher (pending publisher for a new project): project
mmaemu, ownerastding, repositorymmaemu, workflowrelease.yml, environmentpypi. A pending publisher does not reserve the name, and PyPI deletes one that no upload has used within 30 days of its creation (warehouse #19996). Right before step 5, check that https://pypi.org/manage/account/publishing/ still lists it and that https://pypi.org/pypi/mmaemu/json returns 404; if it has gone, add it again with the same fields. -
[by hand] On GitHub:
-
Settings → Environments → New environment
pypi: Required reviewers, yourself. Leave Prevent self-review unticked: the maintainer both pushes the tag and approves the deployment. Optionally, Deployment branches and tags → Selected branches and tags → a tag rulev*. -
Protect
v*tags with a tag ruleset (rulesets replaced GitHub's tag protection rules in 2024): Settings → Rulesets → New ruleset → New tag ruleset; Enforcement status Active (a new ruleset starts as Disabled); target tags matchingv*; Restrict creations, Restrict updates, Restrict deletions and Block force pushes; bypass list Repository admin with Always allow (without a bypass the maintainer's own tag push is rejected). -
Check both (read-only) before step 5:
gh api repos/astding/mmaemu/environments/pypi --jq '.protection_rules' gh api repos/astding/mmaemu/rulesets # lists the ruleset id gh api repos/astding/mmaemu/rulesets/<id>The first must show a
required_reviewersrule listing the maintainer with"prevent_self_review": false; the last"target": "tag","enforcement": "active",refs/tags/v*inconditions.ref_name.include, the rulescreation,update,deletionandnon_fast_forward(Block force pushes) and aRepositoryRolebypass actor with"bypass_mode": "always". Push the tag only when both do: a job that names an environment that does not exist creates it without protection rules, and the publish job would then upload at once, without the checks of step 6.
-
-
[by hand] Push the tag:
git push https://github.com/astding/mmaemu.git refs/tags/v0.1.0. Thereleaseworkflow then:-
job build:
tools/build_fatbin.py --check,tools/check_dist.py --verify-manifest(committed fatbins vs manifest sha256), tag ==mmaemu/_version.py,python -m build --sdist --wheel,twine check --strict,tools/check_dist.py --require-fatbins --tag, and uploads the artifactsdist(wheel and sdist) andrelease-meta(SHA256SUMS); -
job publish waits for the
pypienvironment approval. It is the only job of release.yml and ci.yml withid-token: writeand usespypa/gh-action-pypi-publishpinned by commit SHA (no API token).
-
-
[by hand] Before approving: download the
distandrelease-metaartifacts of that run, one per call so that both land in<download>(gh run download <run-id> -R astding/mmaemu -n dist -D <download>, then the same with-n release-meta), and validate the wheel on the GPU, with the report outside the tree (the committeddocs/release_logs/0.1.0_<gpu>.jsonstays the report of section 3):$GPU timeout 600 python tools/validate_release.py <download>/mmaemu-0.1.0-py3-none-any.whl --out <dir>/t7.jsonCompare its sha256 with the local build and with
SHA256SUMS(build locally with the hatchling version named in the CI wheel'sWHEELGenerator line, otherwise the files differ in that line). Then approve the deployment (the run's Review deployments →pypi→ Approve and deploy) within 30 days of the tag push, GitHub's limit for a job waiting on approval, and before the pending publisher of step 3 expires. -
[by hand] Post-publish smoke test, in fresh venvs:
pip install mmaemu==0.1.0, thenpython -m mmaemu info, once with CPU torch and once with CUDA torch. Then the maintainer's separate evo2-anywhere project pinsmmaemu>=0.1,<0.2. -
[by hand] Archive on Zenodo (a DOI for citing):
-
At zenodo.org, log in with GitHub (or connect GitHub under Linked accounts), open GitHub from the profile menu, click Sync now and switch on
astding/mmaemu(Zenodo lists public repositories only). The repository's Settings → Webhooks then shows a zenodo.org hook for release events. -
On GitHub, Releases → Draft a new release → choose the existing tag
v0.1.0(never create a tag there) → Publish release. A draft that is never published does not reach Zenodo. The wheel, sdist andSHA256SUMSdownloaded in step 6 may be attached. -
Zenodo takes the title, abstract, authors, keywords, licence and message from
CITATION.cffat the tag (validated in section 3) and ignores its other fields: the record's version is the tag name (v0.1.0) and its date the day Zenodo processes the release (normally the release day); both can be edited on the record. -
If the webhook delivery failed, redeliver it (Settings → Webhooks → Recent Deliveries, within 3 days). Do not enable GitHub's immutable releases before the record exists: the tag name of a deleted immutable release cannot be reused, which rules out Zenodo's documented fix for a failed archive (delete the release and create it again).
-
A DOI cannot be reserved before the release. Add the concept DOI (the record's all-versions DOI) to
CITATION.cff(doi:) and to the README in the next snapshot. For 0.1.0 it is 10.5281/zenodo.23440102; a later release keeps it and adds its own version DOI underidentifiersinCITATION.cffand to the README citation once Zenodo has archived it.
-
workflow_dispatch runs the build job only (a dry run of the release); it never publishes.
5. Running the CI steps locally¶
tools/ci_local.sh runs the steps of ci.yml on this machine (CPU only; scratch venvs under
$MMAEMU_CI_DIR, default a temporary directory):
tools/ci_local.sh # lint + T0/T1 (current torch) + NVRTC + dist
tools/ci_local.sh lint dist # selected jobs
MMAEMU_CI_TORCH="2.6.0 2.7.1" tools/ci_local.sh test # T0/T1 against other CPU torch versions
The full Python x torch matrix runs only on GitHub. The documentation site (the build job of docs.yml):
python -m pip install mkdocs==1.6.1 mkdocs-material==9.7.7 # in a scratch venv
mkdocs build --strict --site-dir build/site
6. After a release¶
- 0.1.x: claims for new hardware (e.g. sm_120) are added only with recorded evidence, through the bring-up runbook (docs/runbooks/sm120_bringup.md); a new preset or profile starts as experimental.
- Keep
docs/release_logs/<version>_<gpu>.jsonfor every validated release and GPU.