Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Weekly, grouped dependency updates: a handful of PRs instead of dozens.
# The legacy TFQ stack (legacy/tfq/requirements.txt) is pinned on purpose
# for reproducibility and is deliberately not listed here.
version: 2
updates:
- package-ecosystem: uv
directory: /
schedule: {interval: weekly, day: monday}
open-pull-requests-limit: 5
labels: [dependencies]
groups:
# Separate PR: a PyTorch upgrade can change kernel performance, so check
# it against the benchmark baselines (qnnbench.bench.compare), not just tests.
pytorch:
patterns: ["torch"]
quantum-baselines:
patterns: ["cirq*", "pennylane*"]
dev-tools:
patterns: ["pytest*", "ruff", "mypy", "coverage"]
runtime:
patterns: ["*"]
update-types: [minor, patch]

- package-ecosystem: github-actions
directory: /
schedule: {interval: weekly, day: monday}
labels: [dependencies, ci]
groups:
actions:
patterns: ["*"]

- package-ecosystem: docker
directory: /
schedule: {interval: weekly, day: monday}
labels: [dependencies, docker]
87 changes: 50 additions & 37 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,22 @@ concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

env:
# CPU wheels: CI has no GPU, and the CUDA wheels are ~2 GB larger.
TORCH_INDEX: https://download.pytorch.org/whl/cpu
permissions:
contents: read

# Actions are pinned to commit SHAs (tags can be moved); Dependabot bumps them.
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: astral-sh/setup-uv@v5
- run: uvx ruff check src tests
- run: uvx ruff format --check src tests
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
with:
python-version: "3.12"
- run: uv sync --locked --extra dev --extra serve --extra bench
- run: uv run ruff check src tests
- run: uv run ruff format --check src tests
- run: uv run mypy

test:
runs-on: ubuntu-latest
Expand All @@ -29,51 +33,60 @@ jobs:
matrix:
python: ["3.10", "3.12"]
steps:
- uses: actions/checkout@v5
- uses: astral-sh/setup-uv@v5
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
with:
python-version: ${{ matrix.python }}
activate-environment: true # setup-uv creates and activates .venv
- name: Install
run: |
uv pip install torch --index-url "$TORCH_INDEX"
uv pip install -e ".[dev,serve]"
# --locked fails if uv.lock is out of date with pyproject.toml.
- run: uv sync --locked --extra dev --extra serve --extra bench
- name: Cache MNIST
uses: actions/cache@v4
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: data/mnist.npz
key: mnist-731c5ac6
- name: Tests
run: python -m pytest -m "not slow"
- name: Slow tests (quick benchmark suites, hybrid training)
run: python -m pytest -m slow
- name: Tests with coverage (fails under the floor in pyproject.toml)
run: uv run pytest --cov --cov-report=term --cov-report=xml
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: always()
with:
name: coverage-${{ matrix.python }}
path: coverage.xml

bench-smoke:
# Catches benchmark code that crashes. Timings on shared runners are too
# noisy to gate on; use `qnnbench.bench.compare` on dedicated hardware.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: astral-sh/setup-uv@v5
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
with:
python-version: "3.12"
activate-environment: true
- name: Install
- run: uv sync --locked --extra dev --extra bench
- run: uv run python -m qnnbench.bench qubits batch fusion grad dtype quanv --quick
- run: uv run python -m qnnbench.profile --n-qubits 9 --steps 1 --out runs/profile

audit:
# Known vulnerabilities in the locked dependency set.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
- name: Export locked requirements
# "+cpu" local versions are not on PyPI; audit the same torch release instead.
run: |
uv pip install torch --index-url "$TORCH_INDEX"
uv pip install -e ".[dev,bench]"
- run: python -m qnnbench.bench qubits batch fusion grad dtype quanv --quick
uv export --frozen --no-hashes --no-emit-project \
--extra dev --extra serve --extra bench | sed 's/+cpu//' > requirements-audit.txt
- run: uvx pip-audit -r requirements-audit.txt --no-deps --disable-pip --progress-spinner off

docker:
if: github.event_name == 'push'
k8s:
# Schema-validate every manifest (strict), including the kustomized stack
# and the Kubeflow PyTorchJob CRD.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6
with:
context: .
push: false
tags: qnnbench:ci
cache-from: type=gha
cache-to: type=gha,mode=max
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- name: Install kubeconform
run: |
curl -fsSL https://github.com/yannh/kubeconform/releases/download/v0.8.0/kubeconform-linux-amd64.tar.gz \
| tar xz -C /usr/local/bin kubeconform
python3 -m pip install --quiet pyyaml
- run: k8s/validate.sh
71 changes: 71 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
name: Docker

on:
push:
branches: [main]
tags: ["v*"]
pull_request:
paths: [Dockerfile, .dockerignore, pyproject.toml, "src/**", .github/workflows/docker.yml]

concurrency:
group: docker-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

env:
IMAGE: ghcr.io/${{ github.repository_owner }}/qnnbench

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write # push to GHCR (main and tags only)
security-events: write # upload the Trivy report
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
if: github.event_name == 'push'
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5
with:
images: ${{ env.IMAGE }}
# Immutable sha-<commit> tags for deployments; `main` and semver for humans.
tags: |
type=sha,format=short
type=ref,event=branch
type=semver,pattern={{version}}
- uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
context: .
push: ${{ github.event_name == 'push' }}
load: ${{ github.event_name != 'push' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Scan image
# Report-only for now: the CUDA base image's OS packages carry CVEs we
# don't control. Findings go to the Security tab; tighten to a gate
# (exit-code: 1) once the baseline is reviewed.
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: ${{ fromJSON(steps.meta.outputs.json).tags[0] }} # the sha-<commit> tag
scanners: vuln
severity: HIGH,CRITICAL
ignore-unfixed: true
format: sarif
output: trivy.sarif
continue-on-error: true
- uses: github/codeql-action/upload-sarif@1190a975f95ce23525efb6a3fc21ea29567c1b52 # v3
if: always() && hashFiles('trivy.sarif') != ''
with:
sarif_file: trivy.sarif
category: trivy
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ __pycache__/
.pytest_cache/
.ruff_cache/
.mypy_cache/
coverage.xml
build/
dist/

Expand All @@ -26,3 +27,4 @@ runs/
.vscode/
.idea/
.DS_Store
.coverage
41 changes: 41 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# pre-commit install # then these run on every `git commit`
# legacy/ is the original code, kept as it was for reproducibility.
exclude: ^legacy/
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-added-large-files # the original repo had a committed virtualenv
args: [--maxkb=1024]
exclude: ^uv\.lock$
- id: check-yaml
args: [--allow-multiple-documents] # k8s manifests
- id: check-toml
- id: end-of-file-fixer
- id: trailing-whitespace
- id: detect-private-key
# Local hooks run the tool versions pinned in uv.lock, so they agree with CI.
- repo: local
hooks:
- id: ruff
name: ruff check
entry: uv run ruff check --fix --force-exclude
language: system
types: [python]
- id: ruff-format
name: ruff format
entry: uv run ruff format --force-exclude
language: system
types: [python]
- id: uv-lock
name: uv.lock matches pyproject.toml
entry: uv lock --check
language: system
files: ^(pyproject\.toml|uv\.lock)$
pass_filenames: false
- id: mypy
name: mypy
entry: uv run mypy
language: system
types: [python]
pass_filenames: false
31 changes: 23 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,24 +1,33 @@
# Common tasks. CPU-only torch keeps the local install small; on a GPU box,
# install torch from PyPI (CUDA wheels) instead: `make install TORCH_INDEX=`.
TORCH_INDEX ?= https://download.pytorch.org/whl/cpu
# Common tasks. `make install` uses uv.lock (CPU-only PyTorch, as in CI).
# On a GPU machine use `make install-gpu`: same packages, CUDA PyTorch from PyPI.
PY := .venv/bin/python
EXTRAS := --extra dev --extra bench --extra serve

.PHONY: install test test-all lint format bench bench-quick experiments hybrid serve loadtest docker legacy
.PHONY: install install-gpu hooks test coverage test-all lint format bench bench-quick profile experiments experiments-hybrid hybrid serve loadtest docker legacy

install:
uv venv --python-preference only-managed --python 3.12 .venv
uv pip install --python $(PY) torch $(if $(TORCH_INDEX),--index-url $(TORCH_INDEX))
uv pip install --python $(PY) -e ".[dev,bench,serve]"
uv sync --locked $(EXTRAS)

install-gpu:
uv venv --python 3.12 .venv
uv pip install --python $(PY) --no-sources -e ".[dev,bench,serve]"

hooks:
uv run pre-commit install

test:
$(PY) -m pytest -m "not slow"

coverage:
$(PY) -m pytest --cov --cov-report=term --cov-report=html

test-all:
$(PY) -m pytest

lint:
$(PY) -m ruff check src tests
$(PY) -m ruff format --check src tests
$(PY) -m mypy

format:
$(PY) -m ruff format src tests
Expand All @@ -30,10 +39,16 @@ bench: ## all suites on the default device; results/ <suite>/<device>
bench-quick:
$(PY) -m qnnbench.bench all --quick

profile: ## torch.profiler trace of a training step -> runs/profile/trace.json
$(PY) -m qnnbench.profile

experiments: ## 5-seed accuracy comparison (paper protocol)
$(PY) -m qnnbench.experiments --seeds 5 --epochs 3

hybrid: ## quanvolution vs classical controls on full MNIST
experiments-hybrid: ## quantum filter vs classical controls, 5 seeds
$(PY) -m qnnbench.experiments --suite hybrid --seeds 5 --epochs 5

hybrid: ## quanvolution vs classical controls on full MNIST, one seed
for f in quanv random learned; do \
$(PY) -m qnnbench.hybrid --features $$f --epochs 5 --out results/hybrid/$$f.json; \
done
Expand Down
Loading
Loading