Skip to content

Enable EverestCrystal on the CUDA (Cupy) GPU context#202

Open
InnovateInfinity87 wants to merge 10 commits into
xsuite:mainfrom
InnovateInfinity87:gpu-everest-crystal
Open

Enable EverestCrystal on the CUDA (Cupy) GPU context#202
InnovateInfinity87 wants to merge 10 commits into
xsuite:mainfrom
InnovateInfinity87:gpu-everest-crystal

Conversation

@InnovateInfinity87

@InnovateInfinity87 InnovateInfinity87 commented Jun 9, 2026

Copy link
Copy Markdown

Summary

EverestCrystal (and the Everest scattering + geometry routines it pulls in) now
compiles and tracks on the CUDA context (xobjects.ContextCupy), in addition to
the CPU context. This builds on #790 (which made xtrack's RandomRutherford
GPU-ready, the last CPU-only piece of the Everest physics).

CPU numerics are bitwise unchanged; CPU-vs-GPU agreement is validated
distributionally (KS / KL), the way the Rutherford RNG PR is.

Scope: this enables EverestCrystal on the GPU. EverestBlock and
EverestCollimator are untouched and remain CPU-only.

Background

Historically every Everest test excluded the GPU contexts with the note
# Rutherford RNG not on GPU: the Everest scattering samples the Rutherford
distribution through RandomRutherford, which was CPU-only. #790 lifted that for
the RNG itself; this PR makes the crystal element and its kernels GPU-compilable.

What changed

Grouped as in the commit series:

  • Geometry layer (scattering_routines/geometry/): the per-segment
    function-pointer vtable becomes an int8 enum + switch; segments are stored
    by value instead of on the heap; the crossing/overlap scratch buffers are
    fixed-size stack arrays; qsort is kept on CPU under #ifdef XO_CONTEXT_CPU
    with a GPU insertion-sort fallback; the kill-particle error idiom replaces the
    CPU-only exit.
  • Everest scattering (scattering_routines/everest/): the two
    pair-returning malloc helpers (channel_transport, scamcs) return through
    out-params, and the per-track scratch structs (EverestData, CrystalGeometry,
    EverestCollData) are filled in place on the stack instead of malloc/free.
  • GPU-compile fixes: the bare C99 restrict qualifier becomes the xobjects
    /*restrict*/ token across the crystal compile path (so the specializer drops
    it on CUDA); M_PI is guarded for NVRTC; EverestCrystal_set_material is
    tagged /*gpukern*/.
  • Materials: a Material is landed in the consuming element's context
    before track time (_material_in_context), so MaterialData lives where the
    kernel reads it instead of staying pinned to the host buffer. No-op on CPU.
  • CUDA per-thread stack limit (scattering_routines/everest/gpu_stack.py):
    the crystal kernel's per-track structs exceed CUDA's default 1024-byte
    per-thread stack frame, so a GPU track traps with cudaErrorIllegalAddress.
    A small Cupy-only helper raises cudaLimitStackSize (to a 16 KiB default) once
    per process; it is idempotent, never lowers an existing limit, and is a no-op
    on CPU. It is called from EverestCrystal.__init__ with the element's context,
    so user code needs no extra step.

CPU numerics unchanged (bitwise)

Every change above is either GPU-only or behaviour-preserving on CPU. Verified by
tracking a fixed-seed canonical bunch through Drift + EverestCrystal + Drift on
ContextCpu on this branch vs clean main: the final x, px, y, py, zeta, delta, state arrays are bit-identical (maxdiff = 0).

Validation

  • New test tests/test_everest_crystal_gpu.py:
    • test_everest_crystal_cpu_gpu_distribution_match — tracks the same fixed-seed
      bunch on CPU and GPU and asserts per-coordinate KS < 0.02 plus
      channeling/surviving-fraction agreement. Skips cleanly (importorskip("cupy"))
      where no CUDA GPU is available.
    • test_crystal_stack_limit_is_raised_on_gpu — asserts the per-thread stack is
      raised above the 1024-byte default to the documented ≥ 16 KiB, is a no-op on
      CPU, and is idempotent / never-lowering.
  • Measured on the canonical 20 000-particle line (400 GeV protons, bent silicon
    strip crystal): max KS 5.0e-05 (≈ the 1/N floor, three orders below the
    0.02 gate), KL 0, channeling fraction identical (0.16413), and
    19 911 / 20 000 survivors on both contexts. The deepest-recursion stress
    case (10×-longer crystal, grazing near-pencil beam) also agrees.
  • docs/gpu_everest_crystal.md documents the acceptance method, the stack-limit
    requirement, and the overlaid CPU/GPU plots.

CPU vs GPU on the canonical Drift + EverestCrystal + Drift line — per-coordinate
distributions (top: PDF overlay with KS/KL; middle: empirical CDFs with the KS gap;
bottom: GPU/CPU bin-count ratio against the ±1σ counting-noise band):

Per-coordinate CPU vs GPU distributions

Channeling efficiency (channeled / surviving) vs incoming angle on CPU and GPU, with
the GPU − CPU residual on the right — the curves overlay across the acceptance:

Channeling efficiency CPU vs GPU

Deepest-recursion stress case (10×-longer crystal tracked by a grazing near-pencil
beam) — the hardest case for the per-thread CUDA stack — also agrees per coordinate:

Recursion-stress CPU vs GPU

Performance

This is the first time EverestCrystal can run on the GPU at all.
examples/everest_crystal_gpu_benchmark.py times one turn through the canonical
line on ContextCpu (single core) vs ContextCupy (warm kernels, device
synchronized around the timed region). On one NVIDIA RTX 2070 vs a single CPU
core:

particles CPU [ms] GPU [ms] speed-up
1 000 0.73 1.23 0.6x
10 000 6.26 2.12 3.0x
50 000 31.0 6.06 5.1x
100 000 61.9 11.0 5.6x
200 000 124.1 21.4 5.8x
500 000 309.4 52.1 5.9x

EverestCrystal CPU vs GPU speed-up and throughput

Break-even is ~2–3 k particles; the speed-up plateaus around 5–6× from ~50 k
(GPU ≈ 9.6 Mp/s vs ≈ 1.6 Mp/s on one CPU core). The crystal kernel is branchy and
stochastic (channeling vs amorphous paths, RNG rejection, sub-segment recursion),
so threads diverge and the plateau sits below a non-divergent element; the CPU
baseline is a single core. Absolute numbers are hardware-specific.

Notes

  • Requires the merged RandomRutherford GPU change (#790), now in xtrack main.
  • The comparison plots are committed under docs/ and examples/.

…alue, stack scratch arrays, qsort CPU-guard + GPU insertion sort, kill-particle error idiom. GPU-ready; CPU numerics unchanged.
…alloc->out-params, frees dropped. CPU numerics unchanged.
…fill-via-pointer, EverestCrystal_free removed, missing /*gpufun*/ tags added. CPU numerics unchanged.
… xobjects /*restrict*/ token across the EverestCrystal compile path, so the specializer drops it on CUDA. CPU numerics unchanged (bitwise).
…ext; M_PI guard + /*gpukern*/ set_material tag (clean Cupy compile); get_s.h +1 spare slot (calculate_overlap_array_interval 1-element overrun, GPU-trapped); test fixture create_crystal(NULL,...). CPU numerics bitwise-unchanged; 24/24 crystal+geometry tests pass.
… (per-thread frame exceeds the 1024B default) — fixes the GPU-track cudaErrorIllegalAddress. xcoll EverestCrystal element-level helper: new module xcoll/scattering_routines/everest/gpu_stack.py exposing set_crystal_stack_limit(context, nbytes=16384), exported from that package's __init__.py, and called once at the end of EverestCrystal.__init__ (in xcoll/beam_elements/everest.py) with self._context. Cupy-only, idempotent, never-lowers; no import side-effect.. CPU numerics unchanged; GPU track matches CPU golden.
…enchmark

- examples/everest_crystal_cpu_vs_gpu.py: also generate the T13 recursion-stress
  parity figure (10x crystal + grazing pencil); CPU/GPU both 19796 survivors, KS 5e-5.
- examples/everest_crystal_gpu_benchmark.py (new): warm, device-synced CPU-vs-GPU
  timing of the crystal line across bunch sizes -> speed-up + throughput figure.
- docs/gpu_everest_crystal.md: new 'Recursion-stress agreement' and 'Performance'
  sections. RTX 2070: break-even ~2-3k particles, plateau ~5-6x (5.9x @500k), GPU
  ~9.6 Mp/s vs ~1.6 Mp/s/core.
… change)

- materials/material.py: remove unused public get_materials_context /
  set_materials_context (never called, not exported); keep _materials_context +
  _material_in_context.
- everest/gpu_stack.py: drop the redundant _requested_stack_limit high-water
  global (the live deviceGetLimit already encodes it -> idempotent/never-lower)
  and the defensive _is_cupy_context wrapper; use the xcoll-standard
  isinstance(context, xo.ContextCupy).
- examples: untrack generated everest_crystal_cpu_vs_gpu_summary.json (run output).

GPU tests still pass (distribution match + stack-limit), CPU numerics unchanged.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 385750d116

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

int8_t n_hit = 0;
double* s = (double*) malloc(XC_MAX_CROSS_PER_SEGMENT*n_segments*sizeof(double));
// +1 spare slot (see contract in methods.h:43-44 / first crossing above).
double s[XC_MAX_CROSS_PER_SEGMENT*XC_MAX_SEGMENTS + 1];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Size crossing buffer from n_segments

When callers pass more than four segments, this fixed 9-slot stack buffer is no longer large enough for the existing CPU-only polygon helpers: create_open_polygon is still allowed to build num_polys + 1 segments, and the geometry data generator exercises 8-point open polygons, so find_crossing_with_vlimit can produce 9 hits and then calculate_overlap_array_interval writes the documented spare element at arr[*length]. That turns those valid geometry calls into a stack overwrite; the old malloc(2*n_segments) sizing avoided this.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in d793425. The fixed-size buffer was only correct for the bounded element callers (collimator passes 3 segments, crystal 4 = XC_MAX_SEGMENTS); the geometry-test open polygons exceed that, as flagged. Restored n_segments-based sizing via malloc under #ifdef XO_CONTEXT_CPU (plus the +1 spare slot from the methods.h contract, and the contract comment is updated), and kept the bounded stack buffer on the GPU path where only EverestCrystal (n_segments <= XC_MAX_SEGMENTS) tracks. tests/test_geometry.py now passes 16/16 including the open_polygon cases; the crystal CPU-vs-GPU acceptance and CPU bitwise parity (maxdiff=0) are unchanged.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

hi, any news on this?
@freddieknets

…ENTS polygon overflow)

The malloc->stack GPU port hard-capped the get_s.h crossing buffers at
XC_MAX_CROSS_PER_SEGMENT*XC_MAX_SEGMENTS+1 slots. Element callers stay within that
bound (collimator passes 3 segments, crystal 4 == XC_MAX_SEGMENTS), but the geometry
test generator builds open polygons with more sides (e.g. an 8-point open polygon ->
9 segments), and find_crossing / the calculate_overlap_array_interval spare write then
run past the fixed buffer.

Restore n_segments-based sizing via malloc on CPU (as upstream did, plus the +1 spare
slot), and keep the bounded stack buffer on GPU, where only EverestCrystal
(n_segments <= XC_MAX_SEGMENTS) tracks. CPU numerics stay bitwise-unchanged and the
GPU crystal CPU-vs-GPU acceptance is unchanged; tests/test_geometry.py passes 16/16
(incl. the open-polygon cases) and the everest_crystal GPU tests still pass.
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.

1 participant