Skip to content

CSCS Summer School 2026 event branch#215

Draft
brycelelbach wants to merge 69 commits into
mainfrom
event/2026-07-cscs-summer-school
Draft

CSCS Summer School 2026 event branch#215
brycelelbach wants to merge 69 commits into
mainfrom
event/2026-07-cscs-summer-school

Conversation

@brycelelbach

Copy link
Copy Markdown
Collaborator

Summary

  • merge the July 2026 CSCS Summer School event branch into main
  • add the PyHPC tutorial and CSCS rootless Slurm Container Engine support
  • build and publish the PyHPC image for AMD64 and ARM64 with GitHub Actions

Review

This is a draft so the complete event-branch diff can be reviewed before merge.

Related: #213, #214

aaronj0 and others added 27 commits June 19, 2026 14:22
Port exercises 01, 03, 05, 06, 40, 41, 60 from the accelerated-python
tutorial into tutorials/pyhpc/notebooks (flat, no subdirectories) and
renumber the full set 00-13 in the order:

  00 numpy_intro__ndarray_basics      (AP 01)
  01 numpy_to_cupy__ndarray_basics    (AP 03)
  02 memory_spaces__power_iteration   (AP 05)
  03 mpi4py__collectives              (AP 60)
  04 intro                            (SWE 00)
  05 swe_core__reference_solver       (SWE 01)
  06 jax__lax_scan                    (SWE 02)
  07 pyomp__parallel_for              (SWE 03)
  08 nanobind__cpp_kernel             (SWE 04)
  09 cppjit__gpu_thrust               (SWE 05)
  10 synthesis                        (SWE 06)
  11 asynchrony__power_iteration      (AP 06)
  12 kernel_authoring__copy           (AP 40)
  13 kernel_authoring__book_histogram (AP 41)

Give the SWE notebooks consistent NN__<tech>__<example> names and update
their titles and cross-references for the new numbering. Normalize all
notebooks to canonical format.

Co-Authored-By: Claude <noreply@anthropic.com>
- requirements.txt: add cupy-cuda13x, numba-cuda[cu13], cuda-cccl[test-cu13],
  nvtx, nsightful, mpi4py, and pytest alongside the existing jax/pyomp/
  nanobind/CppJIT stack. All resolve together under Python 3.12.
- dockerfile: rename ACH_TUTORIAL to pyhpc, add OpenMPI (mpirun for notebook
  03) and the OMPI-run-as-root / CuPy cache env vars.
- docker-compose.yml: rename tutorial, dockerfile path, image tag, working-dir.
- test_notebooks.py: expand the ordered ladder to all 14 notebooks (00-13),
  preferring solutions and falling back to exercises, with per-cell timing.
- test.bash + test_packages.py: new, mirroring accelerated-python, so the
  tutorial runs through the standard ACH entrypoint test machinery.
- Track the Shallow_water_waves.gif via Git LFS (was committed as a raw blob).

Co-Authored-By: Claude <noreply@anthropic.com>
- README: cover the full arc (NumPy/CuPy fundamentals, mpi4py, the SWE
  programming-models ladder, and CUDA kernel authoring), grouped into
  Fundamentals / Distributed / Programming models / Kernels with Colab
  badges pointing at the new pyhpc paths and 00-13 numbering. Add a
  Syllabi section and refresh the run instructions.
- Add notebooks/syllabi/pyhpc__numpy_cupy_jax_mpi4py_kernels__8_hours.ipynb
  in the accelerated-python syllabus style (Notebooks + Materials +
  Introduction).

Co-Authored-By: Claude <noreply@anthropic.com>
Notebooks 11-13 %%writefile helper .py scripts and emit Nsight reports,
a downloaded text corpus, and a rendered plot. Ignore those (keeping the
tracked swe_core.py) so a test run leaves the working tree clean.

Co-Authored-By: Claude <noreply@anthropic.com>
The SWE solution notebooks (06-09) import swe_core, but swe_core.py and
swe_step.cpp live in notebooks/ while the solutions execute from
notebooks/solutions/. Under nbclient/JupyterLab only the notebook's own
directory is on sys.path, so the import failed. Symlink both helpers into
solutions/ (swe_core.py resolves __file__ to notebooks/, so timings.json
and swe_step.cpp still resolve to the single shared copy).

This was a latent bug inherited from the original tutorial, whose
solutions were never executed in CI (it shipped no test.bash).

Verified: full suite now passes on an L4 -- test_packages (9) and all 14
notebooks (00-13) green.

Co-Authored-By: Claude <noreply@anthropic.com>
Adapt the build workflow for this fork:
- Trigger on event/** branches (in addition to main and pull-request/N).
- Add a tutorials_filter input (default 'pyhpc') and filter the build
  matrix accordingly, so the fork builds only the pyhpc image rather than
  every tutorial. An empty filter restores the build-everything behavior.

The runner labels (linux-amd64-cpu4 build, linux-amd64-gpu-l4-latest-1
test) are satisfied by a self-hosted L4 runner registered on the fork.

Co-Authored-By: Claude <noreply@anthropic.com>
The build bake uses context ../../.. (repo root), which reads outside the
bake directory and requires an fs.read entitlement. The hardcoded
--allow=fs.read=/home/runner only covers NVIDIA's self-hosted runner home;
on other runners the checkout path differs, so buildx denied it with
'additional privileges requested'. Use --allow=fs.read=* so the build
works regardless of the runner's workspace layout.

Co-Authored-By: Claude <noreply@anthropic.com>
The test job pulls the image from the generated docker-compose, whose
image: anchor is committed as ghcr.io/nvidia/...  The build job, however,
pushes to ghcr.io/<repo_owner>/...  (derived from github.repository_owner).
On a fork these differ, so the test failed pre-pull with 'not found'.

Add an --image-owner option to generate-tagged-docker-composes.py that
rewrites the GHCR owner of the &image anchor, and pass
github.repository_owner from the build workflow. Empty owner is a no-op,
so upstream and sibling tutorials are unaffected.

Co-Authored-By: Claude <noreply@anthropic.com>
…cheme

Rename all PyHPC exercise and solution notebooks to the new
<NN>[__<EXAMPLE>][__<TECH>][__<CONCEPT>] naming scheme and merge the
standalone intro into the reference-solver notebook.

- Merge 04__intro into 05__swe_core__reference_solver as 04__swe__intro
  (framing + NumPy baseline in one notebook); the SWE ladder and the
  kernel/async notebooks shift down by one (05-13 -> 04-12).
- Renumber in-notebook titles, "Next:" pointers, and cross-references.
- Update README tables/prose, the 8-hour syllabus, test_notebooks.py
  ladder, and .gitignore comments to the new names and numbers.

Co-Authored-By: Claude <noreply@anthropic.com>
- Rename the 8-hour syllabus to drop "numpy" and add "cppinterop"
  (pyhpc__cupy_jax_mpi4py_cppinterop_kernels__8_hours.ipynb); update its
  title, internal generated-compose link, and the README Syllabi entry.
- Strip the leading notebook number from the title cells of notebooks
  04-09 and their solutions (titles no longer carry "NN." / "NN ·").
- Add brev/docker-compose.deploy.yml: publishes JupyterLab on 0.0.0.0:8888
  and opens Lab on the renamed syllabus.

Co-Authored-By: Claude <noreply@anthropic.com>
Set each notebook's H1 (and solution H1) to the readable form of its new
<NN>[__EXAMPLE][__TECH][__CONCEPT] filename, in the same component order
(e.g. "Memory Spaces - Power Iteration" -> "Power Iteration - CuPy -
Memory Spaces"; "Kernel Authoring - Copy" -> "Copy - Kernel Authoring";
SWE ladder titled "SWE - <tool>"). Update the matching titles in the
README Notebook column and the syllabus Topic column.

Co-Authored-By: Claude <noreply@anthropic.com>
Merge aaronj0:pyhpc-cppjit-and-interop (PR #2) into the event branch.
The PR branched before our rename/restructure, so on conflicts we keep
our structure and conform the incoming content:

- New interop notebook 14__python_cpp_interop -> 13__cpp_interop (+ solution),
  title set to our scheme ("C++ Interop"); added to README and the syllabus.
- README + test_notebooks ladder conflicts resolved to our numbering/names;
  kept Aaron's CppInterOp beta-release note.
- Preserved Aaron's non-conflicting work: swe_thrust_solver.cpp extraction,
  cppjit notebook edits (now in 08__swe__cppjit__thrust), dockerfile
  python3-config + include path, cffi/memory_profiler deps and package tests,
  swe_core SWE_THRUST_CPP constant.
- docker-compose.deploy.yml: add ?file-browser-path so Lab opens the syllabus
  but the file browser lands in the notebooks dir, not syllabi/.

Co-Authored-By: Aaron Jomy <aaronj0@users.noreply.github.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Reorder the syllabus filename tokens to match the tutorial's actual
progression (CuPy @01 -> mpi4py @03 -> JAX @05):
  pyhpc__cupy_jax_mpi4py_cppinterop_kernels__8_hours
  -> pyhpc__cupy_mpi4py_jax_cppinterop_kernels__8_hours

Updates the syllabus H1 title, its internal generated-compose link, the
README Syllabi entry, and the local deploy compose default URL to match.

Co-Authored-By: Claude <noreply@anthropic.com>
Executing the solution notebooks (e.g. in CI) writes the same generated
artifacts the notebooks/ patterns already ignore — %%writefile scripts,
Nsight reports (.nsys-rep/.sqlite/.ncu-rep), the downloaded corpus, and
generated C/C++ — but into notebooks/solutions/, which the existing
patterns didn't cover. Mirror the patterns there, re-including the
checked-in tutorial source (swe_step.cpp, swe_thrust_solver.cpp).

Co-Authored-By: Claude <noreply@anthropic.com>
@copy-pr-bot

copy-pr-bot Bot commented Jul 21, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

Comment thread tutorials/pyhpc/test/test_packages.py Fixed
Comment thread .github/workflows/build-brev-tutorial-docker-images.yml
Copilot AI review requested due to automatic review settings July 25, 2026 19:19

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 73 out of 77 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (5)

tutorials/pyhpc/test/test_notebooks.py:56

  • If NOTEBOOKS_DIR is missing/empty in an image build, ladder becomes empty and pytest will report a passing test suite (0 parametrized cases). That can mask broken packaging where no notebooks were included.
    tutorials/pyhpc/notebooks/swe_thrust_solver.cpp:36
  • gpu_swe_solve dereferences managed pointers immediately after cudaMallocManaged, but never checks the return codes. If an allocation fails, this can segfault or corrupt outputs; likewise cudaDeviceSynchronize/cudaFree errors are silently ignored (there’s even a TODO comment).
    brev/entrypoint-base-user.bash:26
  • When TURN_USERNAME/TURN_PASSWORD are provided via environment (and the shared credentials file doesn’t exist), the script currently does not write /accelerated-computing-hub/.turn-credentials. Sibling services (nsys/ncu/jupyter) rely on that file unless they also receive TURN_* via their own environment, so they can start without TURN credentials.
if [ -z "${TURN_USERNAME:-}" ] && [ ! -f "${TURN_CREDENTIALS_FILE}" ]; then
  TURN_USERNAME="turn_$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 16)"
  TURN_PASSWORD="$(openssl rand -base64 48 | tr -dc 'a-zA-Z0-9' | head -c 32)"

  echo "TURN_USERNAME=${TURN_USERNAME}" > "${TURN_CREDENTIALS_FILE}"
  echo "TURN_PASSWORD=${TURN_PASSWORD}" >> "${TURN_CREDENTIALS_FILE}"
fi

brev/generate-cscs-edf.bash:91

  • This helper is documented as usable on CSCS systems where Docker isn’t available, but it currently shells out to docker compose ... config to extract x-config fields. On Daint/login nodes without Docker, EDF generation will fail even though this script is part of the CSCS workflow.
if [ -z "${IMAGE}" ] || [ -z "${WORKDIR}" ]; then
    COMPOSE_CONFIG=$(docker compose -f "${COMPOSE_FILE}" config --format json)
fi

docs/cscs.md:44

  • This step implies brev/generate-cscs-edf.bash can be run on CSCS, but the generator currently requires docker compose to read x-config from docker-compose.yml. On CSCS where Docker isn’t available, the guidance should clarify where to run the generator (or the script needs a Docker-free implementation).
For a reproducible run, generate a no-mount EDF pinned to the CI commit tag:

Comment thread brev/dev-common.bash
Comment on lines 143 to 149
if [ "${MOUNT}" = "true" ]; then
echo "🔧 Creating Docker volume (bind mount to local repo)..."
docker volume create --driver local \
echo "🔧 Creating container volume (bind mount to local repo)..."
${ACH_CONTAINER_ENGINE_CMD} volume create --driver local \
--opt type=none \
--opt o=bind \
--opt device="${ACH_REPO_ROOT}" \
"${ACH_TUTORIAL}_accelerated-computing-hub" > /dev/null
Signed-off-by: Bryce Adelstein Lelbach <brycelelbach@gmail.com>
Copilot AI review requested due to automatic review settings July 26, 2026 06:43

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 74 out of 78 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (3)

brev/generate-cscs-edf.bash:91

  • brev/generate-cscs-edf.bash shells out to docker compose ... | jq to read x-config.image and x-config.working-dir. The CSCS docs explicitly instruct running this script on Daint (which typically has no Docker daemon), so this will fail even though the EDF generator is meant for non-Compose environments. Add a fallback that parses these two values directly from the compose YAML when Docker/jq are unavailable (or require --image/--workdir in that case).
if [ -z "${IMAGE}" ] || [ -z "${WORKDIR}" ]; then
    COMPOSE_CONFIG=$(docker compose -f "${COMPOSE_FILE}" config --format json)
fi

tutorials/pyhpc/notebooks/swe_thrust_solver.cpp:36

  • cudaMallocManaged() return values are not checked. If an allocation fails, the subsequent host writes (h[i] = ...) will dereference null/invalid pointers and likely crash, and any prior allocations may leak. Capture and validate the cudaError_t from each allocation and free already-allocated buffers on failure.
    tutorials/pyhpc/notebooks/swe_thrust_solver.cpp:67
  • cudaDeviceSynchronize() return value is ignored. If any kernel launch in the loop fails, the code will still copy back potentially-uninitialized data and then free without surfacing the failure. Check the synchronize result and bail out (after freeing managed allocations) on error.

Copilot AI review requested due to automatic review settings July 26, 2026 07:54

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

Copilot AI review requested due to automatic review settings July 26, 2026 07:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

@brycelelbach

Copy link
Copy Markdown
Collaborator Author

Feedback on the Nsightful cell magic notebook refactorings. Please address all of this in a separate PR targeting this branch:

Notebook 03

"First, we need to ensure the Nsight Systems profiler (nsys), Nsightful, and NVTX are installed and available." => Change to "First, we need to ensure NVIDIA's developer tools are installed and available and do all of our imports."

In the estimate_device_baseline cell, this is wrong:

estimate_device_baseline(
  A_device,
  cfg=PowerIterationConfig(max_steps=1, check_frequency=1, progress=False),
)

The testing should be in a separate code cell, with a "Now let's make sure it works:" text cell right before it. The code cell should look like this:

lam_est_baseline = estimate_device_baseline(A_device)

assert isinstance(lam_est_baseline, (np.ndarray, np.generic)), "Must return a NumPy array or NumPy scalar"
np.testing.assert_allclose(lam_est_baseline, 1, atol=1e-4)

print()
print("Dominant Eigenvalue:", lam_est_baseline)

The testing cell for estimate_device_async current has:

lam_est_async = estimate_device_async(A_device)
np.testing.assert_allclose(lam_est_async, 1, atol=1e-4)

It should have:

lam_est_async = estimate_device_async(A_device)

assert isinstance(lam_est_async, (np.ndarray, np.generic)), "Must return a NumPy array or NumPy scalar"
np.testing.assert_allclose(lam_est_async, 1, atol=1e-4)

print()
print("Dominant Eigenvalue:", lam_est_baseline)

"The timeline is displayed below the profiled cell. Explore what's going on in the program.

EXTRA CREDIT: Download the Nsight Systems GUI and open the report in it to see even more information."
^ The first sentence is inaccurate and was not in the original notebook. What is displayed below the profiled cell is a button to open the results in Perfetto. The second sentence is unnecessary because nsys is available in the container (and it shouldn't be extra credit). Rewrite this cell entirely; it should tell users to explore the profile, either in Perfetto (by clicking the button), by clicking the "+" in list of JupyterLab tabs and then opening Nsight Systems, or by installing the Nsight Systems GUI and downloading the report onto their local system.

"# The native report is saved as power_iteration__baseline.nsys-rep." <= Delete this entire cell.

"This notebook uses the Python 3 (Nsight Systems) kernel so we can profile the baseline in place." <= This is an implementation detail, the user doesn't need to know this. What they need to know is what the %%nsys cell magic does.

"# The native report is saved as power_iteration__async.nsys-rep." <= Delete this entire cell.

Notebook 04

"First, we need to make sure the Nsight Compute profiler, Nsightful, Numba CUDA, and CuPy are available in our notebook:" <= Change to "First, we need to ensure NVIDIA's developer tools are installed and available and do all of our imports."

"The profiling sections require the Python 3 (Nsight Compute) custom kernel provided by the ACH environment. Google Colab cannot select this kernel." <= Delete this text entirely

launch_blocked and launch_optimized are unnecessary, remove them and inline all uses.

There are multiple unnecessary newlines at the end of code cells, clean that up. In some places within a code block, there are blank lines in a row; collapse those to just one blank line.

"The profiling report is displayed directly below the cell above. The first tab summarizes Nsight recommendations and advisories; subsequent tabs contain detailed sections." <= Add a sentence explaining that the Nsight Compute GUI has richer information, charts, and diagrams. Explain that they can access it by clicking the "+" in list of JupyterLab tabs and then opening Nsight Compute, or by installing the Nsight Compute GUI and downloading the report onto their local system.

"EXTRA CREDIT: Download the Nsight Compute GUI and open the report in it to see even more information." <= Delete this sentence.

The further optimization section (7) should have a code cell with some comments providing guidance for the extra credit.

Make sure to reset the copy output before launching the copy kernel when doing correctness checks or profiling, but do NOT do this when benchmarking.

The NCU profiling needs to regex filter for the kernel we want. This was done in the prior version but you dropped support for it.

Notebook 05

There are multiple unnecessary newlines at the end of code cells, clean that up.

Use the same # of items per thread for all histogram variants. It was a mistake that it wasn't this way before. That number should be 8.

This:

bins = 256
values = cp.fromfile("books__15m.txt", dtype=cp.uint8)
histogram = cp.zeros(bins, dtype=cp.int32)
threads_per_block = 512
items_per_thread = 8
items_per_block = threads_per_block * items_per_thread
blocks = len(values) // items_per_block
assert values.size % items_per_block == 0

Should be:

bins = 256

values = cp.fromfile("books__15m.txt", dtype=cp.uint8)
histogram = cp.zeros(bins, dtype=cp.int32)

threads_per_block = 512
items_per_thread = 8
items_per_block = threads_per_block * items_per_thread
blocks = len(values) // items_per_block
assert values.size % items_per_block == 0

Remove launch_global and inline its definition. Inline the histogram[:] = 0, but NOT when benchmarking (fine when profiling).

Remove the cp.cuda.runtime.deviceSynchronize() calls.

Combine the cells that launches the histogram with the cell that prints out the most frequently occurring characters. This occurs for both local and global histograms.

"assert cp.sum(histogram) < len(values), "The intentionally racy kernel should lose updates."" => Replace this with "assert cp.sum(histogram) < len(values)".

The NCU profiling needs to regex filter for the kernel we want. This was done in the prior version but you dropped support for it.

This notebook erroneously refers to "cooperative groups". This is a misunderstanding from a prior contributor. I believe it has been fixed on main. Fix it here too.

Copilot AI review requested due to automatic review settings July 26, 2026 08:02

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

@robobryce

Copy link
Copy Markdown

Addressed all of this feedback in follow-up PR #242: #242

Copilot AI review requested due to automatic review settings July 26, 2026 09:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants