TEST BRANCH ONLY: run the aarch64 bitshuffle exactness test and decode benchmark on the macOS runner
arm64 bitshuffle bench / test:bitshuffle:macos-arm64 (push) Successful in 8s
arm64 bitshuffle bench / test:bitshuffle:macos-arm64 (push) Successful in 8s
Not for merging. The main workflow is removed on this branch so a push runs only this one job: build the standalone bitshuffle test and benchmark with clang, check the decoded image hashes of one EIGER2 and one PILATUS4 chunk against the x86 decoder's, and time classic NEON vs the per-block- malloc fix vs the NEON port of hperf. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D1G8gJVAy6gp1K5Dz3NE5C
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
name: arm64 bitshuffle bench
|
||||
|
||||
# Test branch only: exactness and speed of the aarch64 bitshuffle decode on the Mac mini runner.
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- arm64-bitshuffle-bench
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
bench:
|
||||
name: test:bitshuffle:macos-arm64
|
||||
runs-on: macos-arm64
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Machine
|
||||
shell: bash
|
||||
run: |
|
||||
uname -a
|
||||
sysctl -n machdep.cpu.brand_string hw.perflevel0.logicalcpu hw.perflevel1.logicalcpu || true
|
||||
clang --version | head -1
|
||||
- name: Build
|
||||
shell: bash
|
||||
run: tools/bitshuffle_bench/build.sh "$GITHUB_WORKSPACE" clang clang++
|
||||
- name: Exactness (image hashes must match the x86 decoder)
|
||||
shell: bash
|
||||
run: |
|
||||
cd tools/bitshuffle_bench
|
||||
./bs_exact_test eiger2_0.bslz4 pilatus4_0.bslz4 | tee exact.log
|
||||
grep -q 'image hash 4e45f1d27c1f8992' exact.log
|
||||
grep -q 'image hash 99158ebdd8ebe15e' exact.log
|
||||
grep -q ' 0 failures' exact.log
|
||||
- name: Decode speed (classic NEON vs S1 vs hperf NEON port)
|
||||
shell: bash
|
||||
run: |
|
||||
cd tools/bitshuffle_bench
|
||||
./bs_bench eiger2_0.bslz4 pilatus4_0.bslz4
|
||||
@@ -1,840 +0,0 @@
|
||||
name: Build Packages
|
||||
|
||||
env:
|
||||
# Architecture and link-time optimisation for every Linux configure. Both are deliberately absent
|
||||
# from CMakeLists.txt so a site can pick its own (x86-64-v4 on an AVX-512 cluster, -march=native,
|
||||
# or the plain baseline); CI pins what it ships.
|
||||
#
|
||||
# -flto=auto needs GCC >= 10 and both CI images put gcc-toolset-13 on PATH. Measured on rugnux
|
||||
# against an otherwise identical build: 7-10% fewer retired instructions and a 9% smaller binary,
|
||||
# but only ~1.5% off the wall clock, because the offline pipeline is GPU- and I/O-bound rather
|
||||
# than CPU-instruction-bound. It costs about 3x on an INCREMENTAL rebuild (one file plus link,
|
||||
# 9.8 s -> 30.1 s), which is why it belongs here and not in CMakeLists: CI always builds from
|
||||
# scratch and ships the result, so it pays the link once and a developer never pays it at all.
|
||||
#
|
||||
# Not CMAKE_INTERPROCEDURAL_OPTIMIZATION, which is the tidier spelling and would cover MSVC too:
|
||||
# built and linked fine (CUDA included, and it does not reach .cu either way), but measured 3%
|
||||
# MORE retired instructions than -flto=auto - 396.9 G vs 384.9 G over three runs each against a
|
||||
# 0.45% run-to-run spread, so the gap is real. It also leaves two dependencies (FFTW, libzmq) out
|
||||
# of LTO that the flag reaches, and it did not switch CMAKE_AR to gcc-ar here, so the one
|
||||
# correctness argument for it did not materialise either.
|
||||
LINUX_CMAKE_FLAGS: '-DCMAKE_CXX_FLAGS="-march=x86-64-v3 -flto=auto" -DCMAKE_C_FLAGS="-march=x86-64-v3 -flto=auto"'
|
||||
# MSVC has no spelling for the x86-64-v2 level; /arch:AVX is the nearest and implies SSE4.1/4.2,
|
||||
# which is what matters here - without it Eigen has no vectorised round and falls back to a libm
|
||||
# call per element. AVX is Sandy Bridge (2011) and up, a safe floor for a desktop viewer.
|
||||
MSVC_ARCH_CMAKE_FLAGS: '-DCMAKE_CXX_FLAGS="/arch:AVX" -DCMAKE_C_FLAGS="/arch:AVX"'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
# No pull_request trigger: branches live in this repository, so the push above has already run
|
||||
# the whole workflow on the very same commit. Adding the PR event only ran it a second time.
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
create_release:
|
||||
type: boolean
|
||||
description: 'Create release'
|
||||
required: false
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
# ---- Viewer --------------------------------------------------------------------------------------
|
||||
build-viewer-tgz:
|
||||
name: build:viewer:linux-x86_64:${{ matrix.variant }}
|
||||
# Uploads to the release, so it waits for the job that creates it.
|
||||
needs: create-release
|
||||
runs-on: jfjoch_rocky8
|
||||
container:
|
||||
image: gitea.psi.ch/leonarski_f/jfjoch_rocky8:2609
|
||||
timeout-minutes: 120
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- variant: cuda
|
||||
use_cuda: 'ON'
|
||||
- variant: nocuda
|
||||
use_cuda: 'OFF'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Clean previous build
|
||||
shell: bash
|
||||
run: rm -rf build # self-hosted runners reuse the workspace volume; a stale build dir
|
||||
# can leave the viewer linking a system libcurl instead of the vendored one
|
||||
- name: Configure viewer build
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p build
|
||||
cd build
|
||||
cmake -G Ninja -DJFJOCH_VIEWER_ONLY=ON -DJFJOCH_USE_CUDA=${{ matrix.use_cuda }} ${{ env.LINUX_CMAKE_FLAGS }} -DCMAKE_BUILD_TYPE=Release ..
|
||||
- name: Build viewer tgz
|
||||
shell: bash
|
||||
run: |
|
||||
cd build
|
||||
# The "viewer" component is now jfjoch_viewer alone -- rugnux ships as its own artifact
|
||||
# (build:rugnux-tgz) and the two CLI tools are no longer packaged at all. The full tree is
|
||||
# still built because the viewer links the JFJochRugnux library for its in-process jobs.
|
||||
ninja -j16
|
||||
cpack
|
||||
- name: Upload viewer tgz to release
|
||||
if: github.event_name == 'workflow_dispatch' && (github.event.inputs.create_release == 'true' || github.event.inputs.create_release == true)
|
||||
shell: bash
|
||||
env:
|
||||
TOKEN: ${{ secrets.PIP_REPOSITORY_API_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
shopt -s nullglob
|
||||
# CMakeLists.txt names the archive jfjoch_viewer-<version>-linux-{cuda<major>|cpu}.tar.gz;
|
||||
# rename to .tgz (matching the Windows installer's release naming) before upload.
|
||||
files=(build/jfjoch_viewer-*.tar.gz)
|
||||
if [ ${#files[@]} -eq 0 ]; then
|
||||
echo "No viewer archive found in build/"
|
||||
exit 1
|
||||
fi
|
||||
for file in "${files[@]}"; do
|
||||
tgz="${file%.tar.gz}.tgz"
|
||||
mv "$file" "$tgz"
|
||||
python3 gitea_upload_file.py "$tgz"
|
||||
done
|
||||
|
||||
build-windows:
|
||||
name: build:viewer:windows-x86_64:${{ matrix.variant }}
|
||||
# Uploads to the release, so it waits for the job that creates it.
|
||||
needs: create-release
|
||||
runs-on: windows-11-cuda-qt
|
||||
timeout-minutes: 120
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- variant: cuda
|
||||
use_cuda: 'ON'
|
||||
- variant: nocuda
|
||||
use_cuda: 'OFF'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Configure viewer build
|
||||
shell: cmd
|
||||
run: |
|
||||
for /f "usebackq tokens=*" %%i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -property installationPath`) do set "VSPATH=%%i"
|
||||
call "%VSPATH%\VC\Auxiliary\Build\vcvars64.bat"
|
||||
cmake -G Ninja -B build -DJFJOCH_USE_CUDA=${{ matrix.use_cuda }} -DCMAKE_BUILD_TYPE=Release ${{ env.MSVC_ARCH_CMAKE_FLAGS }} -DCMAKE_PREFIX_PATH="C:/deps;C:/Qt/6.11.1/msvc2022_64"
|
||||
- name: Build viewer
|
||||
shell: cmd
|
||||
run: |
|
||||
for /f "usebackq tokens=*" %%i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -property installationPath`) do set "VSPATH=%%i"
|
||||
call "%VSPATH%\VC\Auxiliary\Build\vcvars64.bat"
|
||||
cmake --build build
|
||||
- name: Build installer (NSIS)
|
||||
shell: cmd
|
||||
run: |
|
||||
for /f "usebackq tokens=*" %%i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -property installationPath`) do set "VSPATH=%%i"
|
||||
call "%VSPATH%\VC\Auxiliary\Build\vcvars64.bat"
|
||||
cd build
|
||||
cpack
|
||||
- name: Upload installer to release
|
||||
if: github.event_name == 'workflow_dispatch' && (github.event.inputs.create_release == 'true' || github.event.inputs.create_release == true)
|
||||
shell: powershell
|
||||
env:
|
||||
TOKEN: ${{ secrets.PIP_REPOSITORY_API_TOKEN }}
|
||||
run: |
|
||||
# NSIS installer named jfjoch-viewer-<version>-win64-{cuda<major>|cpu}.exe (see CMakeLists.txt).
|
||||
$files = Get-ChildItem -Path build -Filter 'jfjoch-viewer-*-win64-*.exe'
|
||||
if ($files.Count -eq 0) { throw 'No Windows installer found in build/' }
|
||||
foreach ($file in $files) {
|
||||
python gitea_upload_file.py $file.FullName
|
||||
if ($LASTEXITCODE -ne 0) { throw "Upload failed for $($file.Name)" }
|
||||
}
|
||||
|
||||
build-macos-viewer:
|
||||
name: build:viewer:macos-arm64:nocuda
|
||||
# Uploads to the release, so it waits for the job that creates it.
|
||||
needs: create-release
|
||||
# A Mac mini running act_runner in host mode (no containers on macOS): the job runs as the runner's
|
||||
# user with that user's tools - Xcode command line tools, CMake.app and Qt under ~/Qt - so the
|
||||
# paths below are this machine's, like the Windows runner's C:/Qt.
|
||||
runs-on: macos-arm64
|
||||
timeout-minutes: 120
|
||||
env:
|
||||
QT_PREFIX: /Users/jungfrau/Qt/6.11.2/macos
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Clean previous build
|
||||
shell: bash
|
||||
run: rm -rf build-macos # host runners keep the workspace between runs
|
||||
- name: Configure viewer build
|
||||
shell: bash
|
||||
# JFJOCH_VIEWER_ONLY is forced on macOS. No -mcpu: the arm64 baseline is what every Apple
|
||||
# Silicon Mac runs, and NEON is part of it.
|
||||
run: cmake -S . -B build-macos -DCMAKE_BUILD_TYPE=Release -DJFJOCH_USE_CUDA=OFF -DCMAKE_PREFIX_PATH="$QT_PREFIX"
|
||||
- name: Build viewer and DMG
|
||||
shell: bash
|
||||
run: |
|
||||
cmake --build build-macos -j "$(sysctl -n hw.ncpu)"
|
||||
cd build-macos && cpack
|
||||
- name: Verify the bundle is self-contained
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
app=$(ls -d build-macos/_CPack_Packages/Darwin/DragNDrop/*/ALL_IN_ONE/jfjoch_viewer.app)
|
||||
# The frameworks sit in the bundle; the executable must look for them there ...
|
||||
otool -l "$app/Contents/MacOS/jfjoch_viewer" > /tmp/jfjv.load
|
||||
if ! grep -q "@executable_path/../Frameworks" /tmp/jfjv.load; then echo "no rpath into the bundle"; exit 1; fi
|
||||
# ... and nothing in the bundle may point at this machine's Qt (or anything else under /Users)
|
||||
find "$app" -type f \( -perm +111 -o -name '*.dylib' \) -exec otool -L {} + > /tmp/jfjv.libs 2>/dev/null || true
|
||||
if grep -q "/Users/" /tmp/jfjv.libs; then grep "/Users/" /tmp/jfjv.libs; echo "absolute path into the build machine"; exit 1; fi
|
||||
# the (ad-hoc) signature must survive everything installed into the bundle
|
||||
codesign --verify --deep --strict "$app"
|
||||
echo "self-contained and validly signed"
|
||||
# Notarization - needs an Apple Developer ID, which the project does not have yet. Once it
|
||||
# does, the app has to be signed with it (hardened runtime, secure timestamp) BEFORE cpack
|
||||
# packs it, e.g. by passing to macdeployqt, through qt_generate_deploy_app_script's
|
||||
# DEPLOY_TOOL_OPTIONS in viewer/CMakeLists.txt:
|
||||
# -codesign="Developer ID Application: <NAME> (<TEAMID>)" -hardened-runtime -timestamp
|
||||
# and the DMG then notarized and stapled here, with an app-specific password stored once on
|
||||
# the runner (xcrun notarytool store-credentials jfjoch-notary ...):
|
||||
# xcrun notarytool submit build-macos/jfjoch-viewer-*.dmg --keychain-profile jfjoch-notary --wait
|
||||
# xcrun stapler staple build-macos/jfjoch-viewer-*.dmg
|
||||
# Until then a downloaded DMG is blocked by Gatekeeper on first open (right-click > Open).
|
||||
- name: Upload DMG to release
|
||||
if: github.event_name == 'workflow_dispatch' && (github.event.inputs.create_release == 'true' || github.event.inputs.create_release == true)
|
||||
shell: bash
|
||||
env:
|
||||
TOKEN: ${{ secrets.PIP_REPOSITORY_API_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
shopt -s nullglob
|
||||
# CMakeLists.txt names it jfjoch-viewer-<version>-macos-arm64.dmg
|
||||
files=(build-macos/jfjoch-viewer-*.dmg)
|
||||
if [ ${#files[@]} -eq 0 ]; then
|
||||
echo "No DMG found in build-macos/"
|
||||
exit 1
|
||||
fi
|
||||
for file in "${files[@]}"; do
|
||||
python3 gitea_upload_file.py "$file"
|
||||
done
|
||||
|
||||
# ---- Rugnux --------------------------------------------------------------------------------------
|
||||
build-rugnux-tgz:
|
||||
name: build:rugnux:linux-x86_64:cuda
|
||||
# Uploads to the release, so it waits for the job that creates it.
|
||||
needs: create-release
|
||||
runs-on: jfjoch_rocky8
|
||||
container:
|
||||
image: gitea.psi.ch/leonarski_f/jfjoch_rocky8:2609
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Clean previous build
|
||||
shell: bash
|
||||
run: rm -rf build-rugnux # self-hosted runners reuse the workspace volume
|
||||
- name: Configure rugnux build
|
||||
shell: bash
|
||||
run: |
|
||||
cmake -G Ninja -S . -B build-rugnux -DJFJOCH_RUGNUX_ONLY=ON -DJFJOCH_USE_CUDA=ON \
|
||||
${{ env.LINUX_CMAKE_FLAGS }} -DCMAKE_BUILD_TYPE=Release
|
||||
- name: Build rugnux tgz
|
||||
shell: bash
|
||||
run: cmake --build build-rugnux --target package -j16
|
||||
- name: Verify the artifact is self-contained
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
rm -rf /tmp/rgx && mkdir -p /tmp/rgx
|
||||
tar xf build-rugnux/rugnux-*.tar.gz -C /tmp/rgx
|
||||
bin=$(find /tmp/rgx -name rugnux -type f)
|
||||
# Each producer writes to a file and the greps read files, rather than piping into
|
||||
# `grep -q`: grep exits at the first match, the producer then dies of SIGPIPE, and under
|
||||
# `set -o pipefail` that becomes the pipeline's status -- which a leading `!` would turn
|
||||
# into a pass, hiding the very thing being tested.
|
||||
ldd "$bin" > /tmp/rgx.ldd 2>&1 || true
|
||||
if grep -q cufft /tmp/rgx.ldd; then echo "cuFFT is linked dynamically"; exit 1; fi
|
||||
find /tmp/rgx -name 'libcufft*' > /tmp/rgx.cufft
|
||||
if [ -s /tmp/rgx.cufft ]; then echo "a libcufft is shipped in the tarball"; exit 1; fi
|
||||
echo "self-contained: static cuFFT, nothing to ship beside it"
|
||||
- name: Upload rugnux tgz to release
|
||||
if: github.event_name == 'workflow_dispatch' && (github.event.inputs.create_release == 'true' || github.event.inputs.create_release == true)
|
||||
shell: bash
|
||||
env:
|
||||
TOKEN: ${{ secrets.PIP_REPOSITORY_API_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
shopt -s nullglob
|
||||
# CMakeLists.txt names the archive rugnux-<version>-linux-{cuda<major>|cpu}.tar.gz;
|
||||
# rename to .tgz before upload, as the viewer archive does.
|
||||
files=(build-rugnux/rugnux-*.tar.gz)
|
||||
if [ ${#files[@]} -eq 0 ]; then
|
||||
echo "No rugnux archive found in build-rugnux/"
|
||||
exit 1
|
||||
fi
|
||||
for file in "${files[@]}"; do
|
||||
tgz="${file%.tar.gz}.tgz"
|
||||
mv "$file" "$tgz"
|
||||
python3 gitea_upload_file.py "$tgz"
|
||||
done
|
||||
|
||||
build-rugnux-arm:
|
||||
name: build:rugnux:linux-aarch64:cuda # cross-compiled on x86_64
|
||||
# Uploads to the release, so it waits for the job that creates it.
|
||||
needs: create-release
|
||||
# Cross-compiled on an x86_64 runner: nvcc, gcc and cmake all run native and only emit aarch64,
|
||||
# so this costs about what a normal build costs. Uses the ordinary ubuntu2404 image, which now
|
||||
# also carries the aarch64 toolchain and the CUDA sbsa cross tree.
|
||||
#
|
||||
# The toolchain file comes from the CHECKOUT, not from the image. It describes how to build this
|
||||
# source, so it belongs with the source: baked into the image it could only be changed by
|
||||
# rebuilding and re-pushing the image, and a fix to it would appear to have no effect.
|
||||
runs-on: jfjoch_ubuntu2404
|
||||
container:
|
||||
image: gitea.psi.ch/leonarski_f/jfjoch_ubuntu2404:2609
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Clean previous build
|
||||
shell: bash
|
||||
run: rm -rf build-aarch64 # self-hosted runners reuse the workspace volume
|
||||
- name: Configure (cross to aarch64 SBSA)
|
||||
shell: bash
|
||||
# NOT ${{ env.LINUX_CMAKE_FLAGS }}: that pins -march=x86-64-v3, which an aarch64 compiler
|
||||
# rejects. The aarch64 baseline already has NEON and a vectorised round, so the Eigen gap
|
||||
# that flag closes on x86 does not exist here.
|
||||
# sm_90 = GH200 (Grace Hopper, e.g. ALPS), sm_121 = GB10 (DGX Spark). One tarball, both.
|
||||
run: |
|
||||
cmake -G Ninja -S . -B build-aarch64 \
|
||||
-DCMAKE_TOOLCHAIN_FILE="$PWD/docker/ubuntu2404/aarch64-sbsa.cmake" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DJFJOCH_RUGNUX_ONLY=ON \
|
||||
-DJFJOCH_USE_CUDA=ON \
|
||||
-DJFJOCH_CUDA_ARCHITECTURES="90;121"
|
||||
- name: Build and package
|
||||
shell: bash
|
||||
run: cmake --build build-aarch64 --target package -j16
|
||||
# A cross build that silently produced x86_64 still packages cleanly, so assert the artifact.
|
||||
- name: Verify the tarball is really aarch64
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
tgz=$(ls build-aarch64/rugnux-*.tar.gz | head -1)
|
||||
echo "checking $tgz"
|
||||
rm -rf /tmp/tgzcheck && mkdir -p /tmp/tgzcheck
|
||||
tar xf "$tgz" -C /tmp/tgzcheck
|
||||
bin=$(find /tmp/tgzcheck -name rugnux -type f | head -1)
|
||||
# Everything is captured to a file first and the greps read files. Piping a large
|
||||
# producer into `grep -q` makes it die of SIGPIPE once grep exits, which `set -o pipefail`
|
||||
# turns into a failure -- and on a negated check into a spurious PASS.
|
||||
file "$bin" > /tmp/tc.bin
|
||||
cat /tmp/tc.bin
|
||||
if ! grep -q "ARM aarch64" /tmp/tc.bin; then echo "not an aarch64 binary"; exit 1; fi
|
||||
# nothing x86 may have leaked in (libjpeg-turbo's ExternalProject is the classic culprit)
|
||||
find /tmp/tgzcheck -type f -exec file {} + > /tmp/tc.arch
|
||||
if grep -q "x86-64" /tmp/tc.arch; then echo "an x86-64 file leaked into the tarball"; exit 1; fi
|
||||
# cuFFT must be linked statically: nothing to ship beside the binary, nothing to find
|
||||
ldd "$bin" > /tmp/tc.ldd 2>&1 || true
|
||||
if grep -q cufft /tmp/tc.ldd; then echo "cuFFT is linked dynamically"; exit 1; fi
|
||||
# both GPU targets present in the fatbin: sm_90 = GH200, sm_121 = GB10
|
||||
cuobjdump --list-elf "$bin" > /tmp/tc.elf
|
||||
for arch in sm_90 sm_121; do
|
||||
if ! grep -q "$arch" /tmp/tc.elf; then echo "fatbin is missing $arch"; exit 1; fi
|
||||
done
|
||||
echo "aarch64, self-contained, both GPU targets present"
|
||||
- name: Upload tarball to release
|
||||
if: github.event_name == 'workflow_dispatch' && (github.event.inputs.create_release == 'true' || github.event.inputs.create_release == true)
|
||||
shell: bash
|
||||
env:
|
||||
TOKEN: ${{ secrets.PIP_REPOSITORY_API_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
shopt -s nullglob
|
||||
files=(build-aarch64/rugnux-*.tar.gz)
|
||||
if [ ${#files[@]} -eq 0 ]; then
|
||||
echo "No rugnux archive found in build-aarch64/"
|
||||
exit 1
|
||||
fi
|
||||
for file in "${files[@]}"; do
|
||||
tgz="${file%.tar.gz}.tgz"
|
||||
mv "$file" "$tgz"
|
||||
python3 gitea_upload_file.py "$tgz"
|
||||
done
|
||||
|
||||
build-rugnux-windows:
|
||||
name: build:rugnux:windows-x86_64:cuda
|
||||
# Uploads to the release, so it waits for the job that creates it.
|
||||
needs: create-release
|
||||
runs-on: windows-11-cuda-qt
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# cmd, not bash: the Windows runner has no bash, and every cmake invocation needs the MSVC
|
||||
# environment from vcvars64.bat first -- same shape as build-windows above. No Qt in
|
||||
# CMAKE_PREFIX_PATH: JFJOCH_RUGNUX_ONLY is the portable subtree minus viewer/, and every
|
||||
# library it needs is now fetched and built by the configure, so C:/deps is only a leftover.
|
||||
- name: Configure rugnux build
|
||||
shell: cmd
|
||||
run: |
|
||||
for /f "usebackq tokens=*" %%i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -property installationPath`) do set "VSPATH=%%i"
|
||||
call "%VSPATH%\VC\Auxiliary\Build\vcvars64.bat"
|
||||
cmake -G Ninja -B build-rugnux -DJFJOCH_RUGNUX_ONLY=ON -DJFJOCH_USE_CUDA=ON -DCMAKE_BUILD_TYPE=Release ${{ env.MSVC_ARCH_CMAKE_FLAGS }} -DCMAKE_PREFIX_PATH="C:/deps"
|
||||
- name: Build rugnux
|
||||
shell: cmd
|
||||
run: |
|
||||
for /f "usebackq tokens=*" %%i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -property installationPath`) do set "VSPATH=%%i"
|
||||
call "%VSPATH%\VC\Auxiliary\Build\vcvars64.bat"
|
||||
cmake --build build-rugnux
|
||||
- name: Build rugnux zip
|
||||
shell: cmd
|
||||
run: |
|
||||
for /f "usebackq tokens=*" %%i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -property installationPath`) do set "VSPATH=%%i"
|
||||
call "%VSPATH%\VC\Auxiliary\Build\vcvars64.bat"
|
||||
cd build-rugnux
|
||||
cpack
|
||||
- name: Upload rugnux zip to release
|
||||
if: github.event_name == 'workflow_dispatch' && (github.event.inputs.create_release == 'true' || github.event.inputs.create_release == true)
|
||||
shell: powershell
|
||||
env:
|
||||
TOKEN: ${{ secrets.PIP_REPOSITORY_API_TOKEN }}
|
||||
run: |
|
||||
# Zip named rugnux-<version>-win64-{cuda<major>|cpu}.zip (see CMakeLists.txt).
|
||||
$files = Get-ChildItem -Path build-rugnux -Filter 'rugnux-*-win64-*.zip'
|
||||
if ($files.Count -eq 0) { throw 'No rugnux zip found in build-rugnux/' }
|
||||
foreach ($file in $files) {
|
||||
python gitea_upload_file.py $file.FullName
|
||||
if ($LASTEXITCODE -ne 0) { throw "Upload failed for $($file.Name)" }
|
||||
}
|
||||
|
||||
build-rugnux-macos:
|
||||
name: build:rugnux:macos-arm64:nocuda
|
||||
# Uploads to the release, so it waits for the job that creates it.
|
||||
needs: create-release
|
||||
runs-on: macos-arm64 # see build-macos-viewer
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Clean previous build
|
||||
shell: bash
|
||||
run: rm -rf build-rugnux-macos
|
||||
- name: Configure rugnux build
|
||||
shell: bash
|
||||
run: cmake -S . -B build-rugnux-macos -DJFJOCH_RUGNUX_ONLY=ON -DJFJOCH_USE_CUDA=OFF -DCMAKE_BUILD_TYPE=Release
|
||||
- name: Build rugnux tgz
|
||||
shell: bash
|
||||
run: cmake --build build-rugnux-macos --target package -j "$(sysctl -n hw.ncpu)"
|
||||
- name: Verify the artifact is self-contained
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
rm -rf /tmp/rgxm && mkdir -p /tmp/rgxm
|
||||
tar xf build-rugnux-macos/rugnux-*.tar.gz -C /tmp/rgxm
|
||||
bin=$(find /tmp/rgxm -name rugnux -type f)
|
||||
# Only the system's own libraries may be linked dynamically; everything else is static.
|
||||
otool -L "$bin" | tail -n +2 > /tmp/rgxm.libs
|
||||
if grep -v -e "/usr/lib/" -e "/System/Library/" /tmp/rgxm.libs; then echo "links a non-system library"; exit 1; fi
|
||||
# and it runs: with no arguments rugnux prints its usage
|
||||
"$bin" > /tmp/rgxm.out 2>&1 || true
|
||||
if ! grep -qi "usage" /tmp/rgxm.out; then cat /tmp/rgxm.out; echo "rugnux did not start"; exit 1; fi
|
||||
echo "self-contained and runs"
|
||||
- name: Upload rugnux tgz to release
|
||||
if: github.event_name == 'workflow_dispatch' && (github.event.inputs.create_release == 'true' || github.event.inputs.create_release == true)
|
||||
shell: bash
|
||||
env:
|
||||
TOKEN: ${{ secrets.PIP_REPOSITORY_API_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
shopt -s nullglob
|
||||
# CMakeLists.txt names it rugnux-<version>-macos-arm64-cpu.tar.gz; rename to .tgz before
|
||||
# upload, as the Linux archives are.
|
||||
files=(build-rugnux-macos/rugnux-*.tar.gz)
|
||||
if [ ${#files[@]} -eq 0 ]; then
|
||||
echo "No rugnux archive found in build-rugnux-macos/"
|
||||
exit 1
|
||||
fi
|
||||
for file in "${files[@]}"; do
|
||||
tgz="${file%.tar.gz}.tgz"
|
||||
mv "$file" "$tgz"
|
||||
python3 gitea_upload_file.py "$tgz"
|
||||
done
|
||||
|
||||
# ---- Jungfraujoch --------------------------------------------------------------------------------
|
||||
unit-tests:
|
||||
name: Unit tests
|
||||
# A release built by hand does not re-run the suite: the push that put the commit on a branch
|
||||
# already ran it on that very commit, and so does the merge to main. Nothing is skipped that
|
||||
# was not already run, and a release build is the one case where the wait is pure delay.
|
||||
if: "!(github.event_name == 'workflow_dispatch' && (github.event.inputs.create_release == 'true' || github.event.inputs.create_release == true))"
|
||||
runs-on: jfjoch_rocky8
|
||||
container:
|
||||
image: gitea.psi.ch/leonarski_f/jfjoch_rocky8:2609
|
||||
options: --gpus all
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
CTEST_OUTPUT_ON_FAILURE: '1'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Configure auth and fetch LFS
|
||||
shell: bash
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.PIP_REPOSITORY_API_TOKEN }}
|
||||
run: |
|
||||
git lfs install --local
|
||||
AUTH=$(git config --local http.${{ github.server_url }}/.extraheader)
|
||||
git config --local --unset http.${{ github.server_url }}/.extraheader
|
||||
git config --local http.${{ github.server_url }}/${{ github.repository }}.git/info/lfs/objects/batch.extraheader "$AUTH"
|
||||
git lfs pull
|
||||
- name: Build tests
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p build
|
||||
cd build
|
||||
cmake -G Ninja -DCMAKE_BUILD_TYPE=Release ${{ env.LINUX_CMAKE_FLAGS }} ..
|
||||
ninja -j16 jfjoch_test jfjoch_hdf5_test jfjoch_hdf5_enospc_test enospc_shim
|
||||
- name: Run unit tests
|
||||
shell: bash
|
||||
run: |
|
||||
cd build/tests
|
||||
./jfjoch_test
|
||||
- name: HDF5 ENOSPC test
|
||||
shell: bash
|
||||
run: |
|
||||
cd build/tests
|
||||
LD_PRELOAD=./enospc_shim.so ./jfjoch_hdf5_enospc_test HDF5File_enospc
|
||||
LD_PRELOAD=./enospc_shim.so ./jfjoch_hdf5_enospc_test FileWriter_enospc
|
||||
- name: Run hdf5 test
|
||||
shell: bash
|
||||
run: |
|
||||
cd build/tools
|
||||
./jfjoch_hdf5_test ../../tests/test_data/compression_benchmark.h5
|
||||
|
||||
build-rpm:
|
||||
name: build:jfjoch:${{ matrix.platform }}:${{ matrix.variant }}
|
||||
# Uploads to the release, so it waits for the job that creates it.
|
||||
needs: create-release
|
||||
runs-on: ${{ matrix.runner }}
|
||||
container:
|
||||
image: ${{ matrix.image }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: jfjoch_rocky8
|
||||
image: gitea.psi.ch/leonarski_f/jfjoch_rocky8:2609
|
||||
distro: rocky8
|
||||
platform: rocky8
|
||||
variant: cuda
|
||||
cmake_flags: -DJFJOCH_USE_CUDA=ON
|
||||
pkg_glob: "*.rpm"
|
||||
upload_url: https://gitea.psi.ch/api/packages/mx/rpm/centos/el8/slsdet8-cuda12/upload
|
||||
- runner: jfjoch_rocky8
|
||||
image: gitea.psi.ch/leonarski_f/jfjoch_rocky8:2609
|
||||
distro: rocky8_sls9
|
||||
platform: rocky8
|
||||
variant: cuda-sls9
|
||||
cmake_flags: -DJFJOCH_USE_CUDA=ON -DSLS9=ON
|
||||
pkg_glob: "*.rpm"
|
||||
upload_url: https://gitea.psi.ch/api/packages/mx/rpm/centos/el8/slsdet9-cuda12/upload
|
||||
- runner: jfjoch_rocky8
|
||||
image: gitea.psi.ch/leonarski_f/jfjoch_rocky8:2609
|
||||
distro: rocky8_nocuda
|
||||
platform: rocky8
|
||||
variant: nocuda
|
||||
cmake_flags: -DJFJOCH_USE_CUDA=OFF
|
||||
pkg_glob: "*.rpm"
|
||||
upload_url: https://gitea.psi.ch/api/packages/mx/rpm/centos/el8/slsdet8-nocuda/upload
|
||||
- runner: jfjoch_rocky9
|
||||
image: gitea.psi.ch/leonarski_f/jfjoch_rocky9:2609
|
||||
distro: rocky9
|
||||
platform: rocky9
|
||||
variant: cuda
|
||||
cmake_flags: -DJFJOCH_USE_CUDA=ON
|
||||
pkg_glob: "*.rpm"
|
||||
upload_url: https://gitea.psi.ch/api/packages/mx/rpm/centos/el9/slsdet8-cuda13/upload
|
||||
- runner: jfjoch_rocky9
|
||||
image: gitea.psi.ch/leonarski_f/jfjoch_rocky9:2609
|
||||
distro: rocky9_sls9
|
||||
platform: rocky9
|
||||
variant: cuda-sls9
|
||||
cmake_flags: -DJFJOCH_USE_CUDA=ON -DSLS9=ON
|
||||
pkg_glob: "*.rpm"
|
||||
upload_url: https://gitea.psi.ch/api/packages/mx/rpm/centos/el9/slsdet9-cuda13/upload
|
||||
- runner: jfjoch_rocky9
|
||||
image: gitea.psi.ch/leonarski_f/jfjoch_rocky9:2609
|
||||
distro: rocky9_nocuda
|
||||
platform: rocky9
|
||||
variant: nocuda
|
||||
cmake_flags: -DJFJOCH_USE_CUDA=OFF
|
||||
pkg_glob: "*.rpm"
|
||||
upload_url: https://gitea.psi.ch/api/packages/mx/rpm/centos/el9/slsdet8-nocuda/upload
|
||||
- runner: jfjoch_ubuntu2204
|
||||
image: gitea.psi.ch/leonarski_f/jfjoch_ubuntu2204:2609
|
||||
distro: ubuntu2204
|
||||
platform: ubuntu2204
|
||||
variant: cuda
|
||||
cmake_flags: -DJFJOCH_USE_CUDA=ON
|
||||
pkg_glob: "*.deb"
|
||||
upload_url: https://gitea.psi.ch/api/packages/mx/debian/pool/jammy/cuda13/upload
|
||||
- runner: jfjoch_ubuntu2204
|
||||
image: gitea.psi.ch/leonarski_f/jfjoch_ubuntu2204:2609
|
||||
distro: ubuntu2204_nocuda
|
||||
platform: ubuntu2204
|
||||
variant: nocuda
|
||||
cmake_flags: -DJFJOCH_USE_CUDA=OFF
|
||||
pkg_glob: "*.deb"
|
||||
upload_url: https://gitea.psi.ch/api/packages/mx/debian/pool/jammy/nocuda/upload
|
||||
- runner: jfjoch_ubuntu2404
|
||||
image: gitea.psi.ch/leonarski_f/jfjoch_ubuntu2404:2609
|
||||
distro: ubuntu2404
|
||||
platform: ubuntu2404
|
||||
variant: cuda
|
||||
cmake_flags: -DJFJOCH_USE_CUDA=ON
|
||||
pkg_glob: "*.deb"
|
||||
upload_url: https://gitea.psi.ch/api/packages/mx/debian/pool/noble/cuda13/upload
|
||||
- runner: jfjoch_ubuntu2404
|
||||
image: gitea.psi.ch/leonarski_f/jfjoch_ubuntu2404:2609
|
||||
distro: ubuntu2404_nocuda
|
||||
platform: ubuntu2404
|
||||
variant: nocuda
|
||||
cmake_flags: -DJFJOCH_USE_CUDA=OFF
|
||||
pkg_glob: "*.deb"
|
||||
upload_url: https://gitea.psi.ch/api/packages/mx/debian/pool/noble/nocuda/upload
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Clean previous build
|
||||
shell: bash
|
||||
run: rm -rf build # self-hosted runners reuse the workspace volume; a stale build dir
|
||||
# can leave the viewer linking a system libcurl instead of the vendored one
|
||||
- name: Setup build (cmake)
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p build
|
||||
cd build
|
||||
cmake -G Ninja -DJFJOCH_INSTALL_DRIVER_SOURCE=ON -DJFJOCH_VIEWER_BUILD=ON ${{ env.LINUX_CMAKE_FLAGS }} -DCMAKE_BUILD_TYPE=Release ${{ matrix.cmake_flags }} ..
|
||||
- name: Build packages
|
||||
shell: bash
|
||||
run: |
|
||||
cd build
|
||||
ninja frontend
|
||||
ninja -j16 package
|
||||
- name: Upload packages
|
||||
if: github.event_name == 'workflow_dispatch' && (github.event.inputs.create_release == 'true' || github.event.inputs.create_release == true)
|
||||
shell: bash
|
||||
env:
|
||||
TOKEN: ${{ secrets.PIP_REPOSITORY_API_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd build
|
||||
shopt -s nullglob
|
||||
files=(${{ matrix.pkg_glob }})
|
||||
if [ ${#files[@]} -eq 0 ]; then
|
||||
echo "No package files found for pattern: ${{ matrix.pkg_glob }}"
|
||||
exit 1
|
||||
fi
|
||||
url="${{ matrix.upload_url }}"
|
||||
if [[ "$url" == *"/rpm/"* ]]; then
|
||||
url="${url}?sign=true"
|
||||
fi
|
||||
for file in "${files[@]}"; do
|
||||
echo "Uploading $file -> $url"
|
||||
curl --fail --user __token__:"$TOKEN" --upload-file "$file" "$url"
|
||||
done
|
||||
|
||||
cd ..
|
||||
|
||||
# The viewer is shipped to the release as a Linux-version-agnostic .tgz by the
|
||||
# build-viewer-tgz job; only the writer (and the XDS plugin .so on rocky8) go to the
|
||||
# release as distro packages here.
|
||||
if [ "${{ matrix.distro }}" = "rocky8_nocuda" ]; then
|
||||
for file in build/jfjoch-writer*rpm build/xds-plugin/libjfjoch_xds_plugin.so.*; do
|
||||
python3 gitea_upload_file.py "$file"
|
||||
done
|
||||
elif [ "${{ matrix.distro }}" = "rocky9_nocuda" ]; then
|
||||
for file in build/jfjoch-writer*rpm; do
|
||||
python3 gitea_upload_file.py "$file"
|
||||
done
|
||||
elif [ "${{ matrix.distro }}" = "ubuntu2204_nocuda" ]; then
|
||||
for file in build/jfjoch*writer*.deb; do
|
||||
python3 gitea_upload_file.py "$file"
|
||||
done
|
||||
fi
|
||||
|
||||
hdf5-consumer-tests:
|
||||
name: HDF5 consumer tests (DIALS, XDS)
|
||||
runs-on: jfjoch_rocky9
|
||||
container:
|
||||
image: gitea.psi.ch/leonarski_f/jfjoch_rocky9:2609
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build processing test
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p build
|
||||
cd build
|
||||
cmake -G Ninja -DCMAKE_BUILD_TYPE=Release ${{ env.LINUX_CMAKE_FLAGS }} ..
|
||||
ninja -j16 jfjoch_hdf5_test jfjoch_xds_plugin
|
||||
# One job for what used to be four, each of which configured and built the same
|
||||
# jfjoch_hdf5_test on the same image. Every case still runs when an earlier one fails -- each
|
||||
# is a subshell under `set -e`, so a case stops at its own first bad command without taking
|
||||
# the rest down -- and the summary at the end decides the job's result.
|
||||
- name: Run every consumer against every file layout
|
||||
shell: bash
|
||||
run: |
|
||||
cd "$GITHUB_WORKSPACE"
|
||||
HDF5_TEST=$PWD/build/tools/jfjoch_hdf5_test
|
||||
DATA=$PWD/tests/test_data/compression_benchmark.h5
|
||||
# Unquoted on use: these are two arguments, not one.
|
||||
CELL="space_group=P43212 unit_cell=78.551,78.551,36.914,90.000,90.000,90.000"
|
||||
|
||||
# -- DIALS -------------------------------------------------------------------------
|
||||
# dials.import before xia2.ssx, checking the image COUNT: dxtbx reports a short scan
|
||||
# rather than failing when it cannot reach every image, and xia2.ssx still succeeds on
|
||||
# the images it did get, so an exit code alone does not notice. Multi-file NXmxLegacy is
|
||||
# exercised by the XDS cases below but deliberately not here: dxtbx reads only the first
|
||||
# data file of a legacy master (it takes one dataset out of NXdata and indexes it
|
||||
# globally), so that combination cannot pass until the limitation is fixed upstream.
|
||||
dials_case() { # <dir> <prefix> <jfjoch_hdf5_test flags...>
|
||||
local dir=$1 prefix=$2; shift 2
|
||||
source /opt/dials-v3-27-0/dials_env.sh
|
||||
rm -rf "$dir"; mkdir -p "$dir"; cd "$dir"
|
||||
"$HDF5_TEST" "$DATA" "$@" -o "$prefix"
|
||||
dials.import "${prefix}_master.h5" | tee import.log
|
||||
grep -q "num images: 100" import.log || { echo "DIALS did not read all 100 images"; return 1; }
|
||||
xia2.ssx image="${prefix}_master.h5" $CELL
|
||||
}
|
||||
dials_legacy() { dials_case test01 legacy -n100; }
|
||||
dials_vds() { dials_case test02 vds -n100 -f25 -V; }
|
||||
# -S writes <prefix>_master.h5, not <prefix>.h5 - naming this wrong meant xia2.ssx was
|
||||
# pointed at a file that never existed, so the integrated format went untested.
|
||||
dials_single() { dials_case test03 single -n100 -S; }
|
||||
# Everything above writes an UNTILTED detector, where every plausible encoding of the tilt
|
||||
# agrees exactly - so none of it constrains the NXmx transformation chain. This does.
|
||||
dials_tilted() {
|
||||
source /opt/dials-v3-27-0/dials_env.sh
|
||||
rm -rf test04; mkdir -p test04; cd test04
|
||||
"$HDF5_TEST" "$DATA" -n1 -S -o geom
|
||||
dials.python "$GITHUB_WORKSPACE/tests/nxmx_geometry_dials_test.py" geom_master.h5
|
||||
}
|
||||
|
||||
# -- XDS, once per read plugin -----------------------------------------------------
|
||||
# Each case clears its directory first. The runner reuses the workspace volume, so a
|
||||
# leftover .h5/.LP from an earlier run could let XDS "pass" on files this case never
|
||||
# wrote -- which is what the first case of two of these jobs used to risk.
|
||||
xds_case() { # <dir> <jfjoch_hdf5_test flags...>
|
||||
local dir=$1; shift
|
||||
cd "tests/$dir"
|
||||
rm -f ./*.h5 ./*.LP ./*.HKL
|
||||
"$HDF5_TEST" "$DATA" "$@"
|
||||
/opt/xds/xds_par
|
||||
test -f IDXREF.LP
|
||||
}
|
||||
durin_legacy() { xds_case xds_durin -n25 -f10; }
|
||||
durin_vds() { xds_case xds_durin -n25 -f10 -V; }
|
||||
durin_single() { xds_case xds_durin -n25 -S; }
|
||||
jfjoch_legacy() { xds_case xds -n25 -f10; }
|
||||
jfjoch_vds() { xds_case xds -n25 -f10 -V; }
|
||||
jfjoch_single() { xds_case xds -n25 -S; }
|
||||
# Neggia cannot read a VDS master, so it gets the two layouts it supports.
|
||||
neggia_legacy() { xds_case xds_neggia -n25 -f10; }
|
||||
neggia_single() { xds_case xds_neggia -n25 -S; }
|
||||
|
||||
fail=0
|
||||
summary=""
|
||||
for t in dials_legacy dials_vds dials_single dials_tilted \
|
||||
durin_legacy durin_vds durin_single \
|
||||
jfjoch_legacy jfjoch_vds jfjoch_single \
|
||||
neggia_legacy neggia_single; do
|
||||
echo "::group::$t"
|
||||
# Run the subshell as a command of its own and test $? afterwards. A subshell used
|
||||
# directly as an `if` condition has its `set -e` ignored, so the case would carry on
|
||||
# past its first failure and report only the last command's status.
|
||||
( set -e; cd "$GITHUB_WORKSPACE"; "$t" )
|
||||
rc=$?
|
||||
if [ "$rc" -eq 0 ]; then
|
||||
summary="$summary PASS $t"$'\n'
|
||||
else
|
||||
summary="$summary FAIL $t (exit $rc)"$'\n'; fail=1
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
done
|
||||
echo "===== HDF5 consumer tests ====="
|
||||
printf '%s' "$summary"
|
||||
exit "$fail"
|
||||
|
||||
python-client:
|
||||
name: Generate python client
|
||||
# Uploads to the release, so it waits for the job that creates it.
|
||||
needs: create-release
|
||||
runs-on: jfjoch_rocky8
|
||||
container:
|
||||
image: gitea.psi.ch/leonarski_f/jfjoch_rocky8:2609
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build python client
|
||||
shell: bash
|
||||
run: bash gen_python_client.sh
|
||||
- name: Upload Package to Gitea PyPI
|
||||
if: github.event_name == 'workflow_dispatch' && (github.event.inputs.create_release == 'true' || github.event.inputs.create_release == true)
|
||||
run: |
|
||||
twine upload -u __token__ -p ${{ secrets.CI_PYPI_TOKEN }} --skip-existing dist/*
|
||||
twine upload -u __token__ -p ${{ secrets.PIP_REPOSITORY_API_TOKEN }} --repository-url https://gitea.psi.ch/api/packages/mx/pypi dist/*
|
||||
|
||||
documentation:
|
||||
name: Build documentation
|
||||
# Uploads to the release, so it waits for the job that creates it.
|
||||
needs: create-release
|
||||
runs-on: jfjoch_rocky8
|
||||
container:
|
||||
image: gitea.psi.ch/leonarski_f/jfjoch_rocky8:2609
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build documentation
|
||||
shell: bash
|
||||
run: bash make_doc.sh
|
||||
- name: Upload documentation
|
||||
if: github.event_name == 'workflow_dispatch' && (github.event.inputs.create_release == 'true' || github.event.inputs.create_release == true)
|
||||
shell: bash
|
||||
run: |
|
||||
git config --global user.email "filip.leonarski@psi.ch"
|
||||
git config --global user.name "Filip Leonarski (Gitea)"
|
||||
git checkout --orphan gitea-pages
|
||||
git reset --hard
|
||||
# Remove everything except .git and public
|
||||
git clean -fdx -e public -e .git
|
||||
|
||||
# Move public contents to root and remove the now-empty public dir
|
||||
shopt -s dotglob nullglob
|
||||
mv public/* .
|
||||
rm -rf public
|
||||
git add .
|
||||
git commit -m "Deploy site"
|
||||
git push -f https://${{secrets.GITHUB_TOKEN}}@gitea.psi.ch/${{ github.repository }}.git gitea-pages
|
||||
|
||||
# ---- Release -------------------------------------------------------------------------------------
|
||||
# Runs on every event, but only creates something when asked. Keeping it unconditional is what
|
||||
# lets every other job depend on it with a plain `needs:` -- a skipped dependency would otherwise
|
||||
# skip its dependents too. When it creates nothing it is just a checkout.
|
||||
create-release:
|
||||
name: Create release
|
||||
runs-on: jfjoch_rocky8
|
||||
container:
|
||||
image: gitea.psi.ch/leonarski_f/jfjoch_rocky8:2609
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: 'true' # Optional; should be the default
|
||||
# The LFS objects are pulled only to attach the FPGA images below, so this stays off the
|
||||
# critical path of an ordinary push -- where this job is a bare checkout and nothing else.
|
||||
- name: Configure auth and fetch LFS
|
||||
if: github.event_name == 'workflow_dispatch' && (github.event.inputs.create_release == 'true' || github.event.inputs.create_release == true)
|
||||
shell: bash
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.PIP_REPOSITORY_API_TOKEN }}
|
||||
run: |
|
||||
git lfs install --local
|
||||
AUTH=$(git config --local http.${{ github.server_url }}/.extraheader)
|
||||
git config --local --unset http.${{ github.server_url }}/.extraheader
|
||||
git config --local http.${{ github.server_url }}/${{ github.repository }}.git/info/lfs/objects/batch.extraheader "$AUTH"
|
||||
git lfs pull
|
||||
- name: Create release
|
||||
if: github.event_name == 'workflow_dispatch' && (github.event.inputs.create_release == 'true' || github.event.inputs.create_release == true)
|
||||
env:
|
||||
TOKEN: ${{ secrets.PIP_REPOSITORY_API_TOKEN }}
|
||||
shell: bash
|
||||
run: |
|
||||
python3 gitea_create_release.py
|
||||
@@ -0,0 +1,131 @@
|
||||
// Bitshuffle block-decode benchmark: classic NEON (malloc per block), classic NEON with the caller's
|
||||
// scratch (S1), the NEON port of hperf, hperf itself (scalar fallback on arm64), and the selector
|
||||
// rugnux uses (JFJochBitUnshuffleBlock). Optionally the full bitshuffle/LZ4 chunk decode.
|
||||
//
|
||||
// Build: ./build.sh /path/to/nextgendcu [clang] [clang++] (checkout of branch arm64-bitshuffle), or by hand:
|
||||
// REPO=/path/to/nextgendcu
|
||||
// S="$REPO/compression/bitshuffle/bitshuffle_core.c $REPO/compression/bitshuffle/iochain.c \
|
||||
// $REPO/compression/bitshuffle_hperf/bitshuffle.c $REPO/compression/bitshuffle_hperf/bitshuffle_neon.c \
|
||||
// $REPO/compression/lz4/lz4.c"
|
||||
// for f in $S; do clang -O3 -std=gnu11 -I$REPO/compression -c $f -o $(basename $f .c).o; done
|
||||
// clang++ -O3 -std=c++20 -I$REPO/compression bs_bench.cpp *.o -o bs_bench
|
||||
// ./bs_bench [-m MB] [chunk.bslz4 ...] # MB of synthetic blocks (default 64); 4-byte chunks,
|
||||
// # e.g. eiger2_0.bslz4 pilatus4_0.bslz4
|
||||
// (on Linux use gcc/g++ the same way; the same objects also build bs_exact_test.cpp, the
|
||||
// byte-identity check, which is worth running natively once: ./bs_exact_test *.bslz4)
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <BitShuffleBlock.h>
|
||||
#include <lz4/lz4.h>
|
||||
extern "C" uint64_t bshuf_read_uint64_BE(const void *buf);
|
||||
#if defined(__aarch64__)
|
||||
#include <bitshuffle_hperf/bitshuffle_neon.h>
|
||||
#endif
|
||||
|
||||
using Decoder = std::function<void(char *out, const char *in, char *scratch, size_t size, size_t elem)>;
|
||||
|
||||
static double best_seconds(const std::function<void()> &f, int reps) {
|
||||
double best = 1e30;
|
||||
for (int r = 0; r < reps; r++) {
|
||||
auto t0 = std::chrono::steady_clock::now();
|
||||
f();
|
||||
best = std::min(best, std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count());
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
static std::vector<std::pair<std::string, Decoder>> decoders() {
|
||||
std::vector<std::pair<std::string, Decoder>> d;
|
||||
d.push_back({"classic bshuf_untrans_bit_elem", [](char *o, const char *i, char *, size_t n, size_t e) {
|
||||
bshuf_untrans_bit_elem(i, o, n, e);
|
||||
}});
|
||||
d.push_back({"hperf bitshuf_decode_block", [](char *o, const char *i, char *s, size_t n, size_t e) {
|
||||
bitshuf_decode_block(o, i, s, n, e);
|
||||
}});
|
||||
#if defined(__aarch64__)
|
||||
d.push_back({"S1 classic NEON, caller scratch", [](char *o, const char *i, char *s, size_t n, size_t e) {
|
||||
bshuf_trans_byte_bitrow_NEON(i, s, n, e);
|
||||
bshuf_shuffle_bit_eightelem_NEON(s, o, n, e);
|
||||
}});
|
||||
d.push_back({"NEON port of hperf", [](char *o, const char *i, char *s, size_t n, size_t e) {
|
||||
bitshuf_decode_block_neon(o, i, s, n, e);
|
||||
}});
|
||||
#endif
|
||||
d.push_back({"JFJochBitUnshuffleBlock (shipped)", [](char *o, const char *i, char *s, size_t n, size_t e) {
|
||||
JFJochBitUnshuffleBlock(o, i, s, n, e);
|
||||
}});
|
||||
return d;
|
||||
}
|
||||
|
||||
// Block transform alone: 8192-byte blocks (the EIGER/rugnux default) over `total` bytes.
|
||||
static void bench_blocks(size_t total) {
|
||||
uint64_t x = 12345;
|
||||
for (size_t elem : {1, 2, 4}) {
|
||||
const size_t block = 8192 / elem;
|
||||
std::vector<char> raw(total), enc(total), out(total), scratch(block * elem);
|
||||
for (size_t i = 0; i < total / elem; i++) { // sparse photon counts
|
||||
x = x * 6364136223846793005ULL + 1442695040888963407ULL;
|
||||
const uint32_t v = (x >> 60) < 3 ? (uint32_t) ((x >> 40) % 17) : 0;
|
||||
memcpy(&raw[i * elem], &v, elem);
|
||||
}
|
||||
for (size_t off = 0; off < total; off += block * elem)
|
||||
bshuf_trans_bit_elem(&raw[off], &enc[off], block, elem);
|
||||
printf("block decode, elem_size %zu, block %zu elements, %zu MB:\n", elem, block, total >> 20);
|
||||
for (auto &[name, dec] : decoders()) {
|
||||
memset(out.data(), 0, total);
|
||||
const double t = best_seconds([&] {
|
||||
for (size_t off = 0; off < total; off += block * elem)
|
||||
dec(&out[off], &enc[off], scratch.data(), block, elem);
|
||||
}, 5);
|
||||
printf(" %-36s %8.0f MB/s %s\n", name.c_str(), total / t / 1e6,
|
||||
memcmp(out.data(), raw.data(), total) == 0 ? "" : " MISMATCH");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Full bitshuffle/LZ4 chunk decode (LZ4 + block transform), as JFJochDecompressHperfPtr does it.
|
||||
static void bench_chunk(const std::string &path, size_t elem) {
|
||||
std::ifstream f(path, std::ios::binary);
|
||||
std::vector<char> src((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||
if (src.size() < 12) { printf("cannot read %s\n", path.c_str()); return; }
|
||||
const uint8_t *p = (const uint8_t *) src.data();
|
||||
const size_t total = bshuf_read_uint64_BE(p), block = bshuf_read_uint32_BE(p + 8) / elem, nelem = total / elem;
|
||||
std::vector<char> image(total), dec(block * elem), scratch(block * elem), ref;
|
||||
|
||||
printf("chunk %s (%zu MB decoded):\n", path.c_str(), total >> 20);
|
||||
for (auto &[name, fn] : decoders()) {
|
||||
const double t = best_seconds([&] {
|
||||
size_t pos = 12, done = 0;
|
||||
while (nelem - done >= 8) {
|
||||
size_t cur = std::min(block, nelem - done);
|
||||
cur -= cur % 8;
|
||||
const size_t csize = bshuf_read_uint32_BE(p + pos);
|
||||
pos += 4;
|
||||
LZ4_decompress_safe(src.data() + pos, dec.data(), (int) csize, (int) (cur * elem));
|
||||
pos += csize;
|
||||
fn(&image[done * elem], dec.data(), scratch.data(), cur, elem);
|
||||
done += cur;
|
||||
}
|
||||
memcpy(&image[done * elem], src.data() + pos, (nelem - done) * elem);
|
||||
}, 5);
|
||||
if (ref.empty()) ref = image;
|
||||
printf(" %-36s %8.0f MB/s %s\n", name.c_str(), total / t / 1e6, image == ref ? "" : " MISMATCH");
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
int first = 1;
|
||||
size_t mb = 64;
|
||||
if (argc > 2 && strcmp(argv[1], "-m") == 0) { mb = strtoul(argv[2], nullptr, 10); first = 3; }
|
||||
bench_blocks(mb << 20);
|
||||
for (int i = first; i < argc; i++)
|
||||
bench_chunk(argv[i], 4);
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
// Exactness test for the bitshuffle block decoders: classic (malloc), S1 (caller scratch),
|
||||
// hperf-NEON port, hperf (x86 SIMD or scalar), and the shipped JFJochBitUnshuffleBlock selector.
|
||||
// Prints a hash over every encoded input and decoded output so x86 and aarch64 runs can be compared.
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cstdlib>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <fstream>
|
||||
|
||||
#include <BitShuffleBlock.h>
|
||||
#include <lz4/lz4.h>
|
||||
extern "C" uint64_t bshuf_read_uint64_BE(const void* buf);
|
||||
#if defined(__aarch64__)
|
||||
#include <bitshuffle_hperf/bitshuffle_neon.h>
|
||||
#endif
|
||||
|
||||
static uint64_t rng_state = 0x1234567887654321ULL;
|
||||
static uint64_t next_rand() {
|
||||
uint64_t z = (rng_state += 0x9e3779b97f4a7c15ULL);
|
||||
z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9ULL;
|
||||
z = (z ^ (z >> 27)) * 0x94d049bb133111ebULL;
|
||||
return z ^ (z >> 31);
|
||||
}
|
||||
|
||||
static uint64_t hash = 1469598103934665603ULL;
|
||||
static void hash_bytes(const char *p, size_t n) {
|
||||
for (size_t i = 0; i < n; i++) { hash ^= (uint8_t) p[i]; hash *= 1099511628211ULL; }
|
||||
}
|
||||
|
||||
static long failures = 0, checks = 0;
|
||||
static void expect_same(const std::vector<char> &ref, const std::vector<char> &got, const char *what,
|
||||
size_t size, size_t elem, int pattern) {
|
||||
checks++;
|
||||
if (memcmp(ref.data(), got.data(), ref.size()) != 0) {
|
||||
if (failures < 20)
|
||||
printf("MISMATCH %s size=%zu elem=%zu pattern=%d\n", what, size, elem, pattern);
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
|
||||
static void fill(std::vector<char> &v, size_t elem, int pattern) {
|
||||
const size_t n = v.size() / elem;
|
||||
for (size_t i = 0; i < v.size(); i++) {
|
||||
switch (pattern) {
|
||||
case 0: v[i] = (char) next_rand(); break; // random
|
||||
case 1: v[i] = 0; break; // zeros
|
||||
case 2: v[i] = (char) 0xff; break; // ones
|
||||
case 3: v[i] = (char) ((i % elem == 0) ? (next_rand() % 7 == 0 ? next_rand() % 5 : 0) : 0); break; // sparse counts
|
||||
case 4: v[i] = (char) (i & 1 ? 0xaa : 0x55); break; // alternating
|
||||
case 5: v[i] = (char) (((i / elem) % (8 * elem)) / 8 == i % elem ? 1u << ((i / elem) % 8) : 0); break; // walking bit
|
||||
case 6: v[i] = (char) ((i % elem == elem - 1) ? 0x80 : (char) next_rand()); break; // sign bit set + noise
|
||||
}
|
||||
}
|
||||
(void) n;
|
||||
}
|
||||
|
||||
static void run_block(size_t size, size_t elem, int pattern) {
|
||||
std::vector<char> orig(size * elem), enc(size * elem), enc_h(size * elem), scratch(size * elem);
|
||||
fill(orig, elem, pattern);
|
||||
|
||||
if (bshuf_trans_bit_elem(orig.data(), enc.data(), size, elem) < 0) { printf("encode failed\n"); failures++; return; }
|
||||
if (bitshuf_encode_block(enc_h.data(), orig.data(), scratch.data(), size, elem) != 0) { printf("hperf encode failed\n"); failures++; return; }
|
||||
expect_same(enc, enc_h, "encode classic vs hperf", size, elem, pattern);
|
||||
hash_bytes(enc.data(), enc.size());
|
||||
|
||||
std::vector<char> out(size * elem);
|
||||
|
||||
std::fill(out.begin(), out.end(), 0x5a);
|
||||
bshuf_untrans_bit_elem(enc.data(), out.data(), size, elem);
|
||||
expect_same(orig, out, "classic bshuf_untrans_bit_elem", size, elem, pattern);
|
||||
|
||||
std::fill(out.begin(), out.end(), 0x5a);
|
||||
bitshuf_decode_block(out.data(), enc.data(), scratch.data(), size, elem);
|
||||
expect_same(orig, out, "hperf bitshuf_decode_block", size, elem, pattern);
|
||||
|
||||
std::fill(out.begin(), out.end(), 0x5a);
|
||||
std::fill(scratch.begin(), scratch.end(), 0x33);
|
||||
if (JFJochBitUnshuffleBlock(out.data(), enc.data(), scratch.data(), size, elem) < 0) failures++;
|
||||
expect_same(orig, out, "JFJochBitUnshuffleBlock", size, elem, pattern);
|
||||
|
||||
#if defined(__aarch64__)
|
||||
std::fill(out.begin(), out.end(), 0x5a);
|
||||
bshuf_trans_byte_bitrow_NEON(enc.data(), scratch.data(), size, elem);
|
||||
bshuf_shuffle_bit_eightelem_NEON(scratch.data(), out.data(), size, elem);
|
||||
expect_same(orig, out, "S1 two-stage NEON with scratch", size, elem, pattern);
|
||||
|
||||
if (elem == 1 || elem == 2 || elem == 4) {
|
||||
std::fill(out.begin(), out.end(), 0x5a);
|
||||
if (bitshuf_decode_block_neon(out.data(), enc.data(), scratch.data(), size, elem) != 0) failures++;
|
||||
expect_same(orig, out, "hperf NEON port", size, elem, pattern);
|
||||
} else if (bitshuf_decode_block_neon(out.data(), enc.data(), scratch.data(), size, elem) != -1)
|
||||
failures++;
|
||||
#endif
|
||||
hash_bytes(out.data(), out.size());
|
||||
}
|
||||
|
||||
// A bitshuffle/LZ4 chunk as HDF5 filter 32008 stores it, decoded block by block through every path.
|
||||
static void run_chunk(const std::string &path, size_t elem) {
|
||||
std::ifstream f(path, std::ios::binary);
|
||||
std::vector<char> src((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||
const uint8_t *p = (const uint8_t *) src.data();
|
||||
const size_t total = bshuf_read_uint64_BE(p);
|
||||
const size_t block = bshuf_read_uint32_BE(p + 8) / elem;
|
||||
const size_t nelem = total / elem;
|
||||
size_t pos = 12, done = 0;
|
||||
std::vector<char> image(total);
|
||||
std::vector<char> dec(block * elem), scratch(block * elem), ref(block * elem), out(block * elem);
|
||||
long nblocks = 0, npartial = 0;
|
||||
while (nelem - done >= 8) {
|
||||
size_t cur = std::min(block, nelem - done);
|
||||
cur -= cur % 8;
|
||||
const size_t csize = bshuf_read_uint32_BE(p + pos);
|
||||
pos += 4;
|
||||
const int r = LZ4_decompress_safe(src.data() + pos, dec.data(), (int) csize, (int) (cur * elem));
|
||||
if (r != (int) (cur * elem)) { printf("LZ4 error %s\n", path.c_str()); failures++; return; }
|
||||
pos += csize;
|
||||
if (cur != block) npartial++;
|
||||
nblocks++;
|
||||
|
||||
bshuf_untrans_bit_elem(dec.data(), ref.data(), cur, elem);
|
||||
ref.resize(cur * elem); out.resize(cur * elem);
|
||||
|
||||
bitshuf_decode_block(out.data(), dec.data(), scratch.data(), cur, elem);
|
||||
expect_same(ref, out, "chunk hperf", cur, elem, -1);
|
||||
JFJochBitUnshuffleBlock(out.data(), dec.data(), scratch.data(), cur, elem);
|
||||
expect_same(ref, out, "chunk JFJochBitUnshuffleBlock", cur, elem, -1);
|
||||
#if defined(__aarch64__)
|
||||
bshuf_trans_byte_bitrow_NEON(dec.data(), scratch.data(), cur, elem);
|
||||
bshuf_shuffle_bit_eightelem_NEON(scratch.data(), out.data(), cur, elem);
|
||||
expect_same(ref, out, "chunk S1", cur, elem, -1);
|
||||
bitshuf_decode_block_neon(out.data(), dec.data(), scratch.data(), cur, elem);
|
||||
expect_same(ref, out, "chunk NEON port", cur, elem, -1);
|
||||
#endif
|
||||
memcpy(image.data() + done * elem, ref.data(), cur * elem);
|
||||
ref.resize(block * elem); out.resize(block * elem);
|
||||
done += cur;
|
||||
}
|
||||
const size_t left = (nelem - done) * elem;
|
||||
memcpy(image.data() + done * elem, src.data() + pos, left);
|
||||
pos += left;
|
||||
if (pos != src.size()) { printf("chunk not fully consumed %s\n", path.c_str()); failures++; }
|
||||
uint64_t h = 1469598103934665603ULL;
|
||||
for (char c : image) { h ^= (uint8_t) c; h *= 1099511628211ULL; }
|
||||
printf("chunk %s: %zu elements, %ld blocks (%ld partial), %zu leftover bytes, image hash %016llx\n",
|
||||
path.c_str(), nelem, nblocks, npartial, left, (unsigned long long) h);
|
||||
hash_bytes(image.data(), image.size());
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const size_t elems[] = {1, 2, 3, 4, 8};
|
||||
std::vector<size_t> sizes;
|
||||
for (size_t s = 8; s <= 1040; s += 8) sizes.push_back(s); // every row-length tail 1..130 bytes
|
||||
for (size_t s : {2048, 2056, 4096, 8192, 8200, 16384, 1544, 1536, 2040})
|
||||
sizes.push_back(s);
|
||||
for (size_t elem : elems)
|
||||
for (size_t size : sizes)
|
||||
for (int pattern = 0; pattern < 7; pattern++)
|
||||
run_block(size, elem, pattern);
|
||||
for (int i = 1; i < argc; i++)
|
||||
run_chunk(argv[i], 4);
|
||||
printf("%ld checks, %ld failures, total hash %016llx\n", checks, failures, (unsigned long long) hash);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
# ./build.sh <REPO> [CC] [CXX] - builds bs_bench and bs_exact_test next to this script
|
||||
set -e
|
||||
REPO=$1; CC=${2:-cc}; CXX=${3:-c++}
|
||||
cd "$(dirname "$0")"
|
||||
for f in bitshuffle/bitshuffle_core.c bitshuffle/iochain.c bitshuffle_hperf/bitshuffle.c bitshuffle_hperf/bitshuffle_neon.c lz4/lz4.c; do
|
||||
$CC -O3 -std=gnu11 -I$REPO/compression -c $REPO/compression/$f -o $(basename $f .c).o
|
||||
done
|
||||
$CXX -O3 -std=c++20 -I$REPO/compression bs_bench.cpp *.o -o bs_bench
|
||||
$CXX -O2 -std=c++20 -I$REPO/compression bs_exact_test.cpp *.o -o bs_exact_test
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user