The Journey of Scaling Up: From Local Prototyping to Automated Cluster Scaling
Dr. Recep Erol β’ Computational Scientist for AI
Center for Research Computing (CRC) β’ Rice University
Workshop Date: October 2026
ποΈ Center for Research Computing (CRC) β’ Rice University
π§ re51@rice.edu
π linkedin.com/in/erolrecep
π» github.com/erolrecep
π erolrecep.github.io
git and make installed.git clone \
https://github.com/erolrecep/scientific_python_workflow_hpc.git
cd scientific_python_workflow_hpc
Transitioning from ad-hoc scripts to disciplined, reproducible workflows on High-Performance Computing systems.
uv, micromamba) over legacy hacks.pip install and Anaconda fail on HPC clusters:
Standard pip struggles to resolve complex compiled C/C++, CUDA, and Fortran extensions
without compiler mismatches on CentOS/Rocky Linux nodes.
Installing full Anaconda/Miniforge extracts 30,000+ tiny files into $HOME.
Shared Lustre/GPFS filesystems impose strict inode caps, locking user accounts.
Traditional Conda SAT solvers take 10+ minutes to resolve dependency graphs, burning costly interactive allocation time.
uv (Rust)
and micromamba (C++).
uv & micromambauv (Pure Python Speed)uv venv .venv
uv pip install -r requirements.txt
micromamba (Compiled Binaries)~/.bashrc with
conda init.
micromamba create -p ./.mamba-env \
-c conda-forge python=3.10
./.mamba-env/bin/pip install -r requirements.txt
make all-uv and make all-mamba
automatically detect if the binary is missing, download it into ~/.local/bin, and execute
seamlessly!
Every line in a Makefile runs inside an independent subshell. Sourced virtualenvs vanish instantly!
# β THIS FAILS SILENTLY:
process:
source .venv/bin/activate
python src/process_images.py
# (python is now system python, not .venv!)
Target the environment's python executable explicitly. Idempotent, robust, and cluster-proof:
PYTHON = .venv/bin/python
# β
THIS SUCCEEDS EVERY TIME:
process: env data
$(PYTHON) src/process_images.py
check-env β env (.venv) β data (download/extract) β
process β test β profile
Make ensures intermediate artifacts are never recomputed unless source code or dependencies change.
Structured progression from local environment isolation to multi-core CPU, GPU batching, and cluster execution.
uv and micromamba auto-provisioning..sbatch.Dataset: Oxford VGG 17flowers (17 categories, 80 images each, variable resolutions)
Flattens 3-channel RGB image to 1-channel luminance:
Y = 0.299Β·R + 0.587Β·G + 0.114Β·B
Strips redundant color channels while preserving textural edges.
Custom edge-enhancing sharpening filter:
Emphasizes high-frequency gradients across flower petal boundaries.
Normalizes variable-sized images to fixed shape:
Target: (229, 229, 1) β’ uint8 [0, 255]
Strict input tensor dimension for downstream deep neural networks.
All code, data download scripts, and batch profiles are open-source and ready to clone.
Transition Python workloads from local laptops to reproducible, automated High-Performance Computing systems.
git clone https://github.com/erolrecep/scientific_python_workflow_hpc.git
cd scientific_python_workflow_hpc
make all-uv # or: make all-mamba
| Comparison Pair | Mean Pixel Difference | Maximum Absolute Difference | Root Cause |
|---|---|---|---|
| OpenCV vs. Pillow | 11.31 pixels |
135.0 pixels | Differing sub-pixel interpolation offsets & kernel weights |
| OpenCV vs. PyTorch | 12.09 pixels |
140.9 pixels | align_corners coordinate grid mapping differences |
| Pillow vs. PyTorch | 1.01 pixels |
12.8 pixels | Rounding floating-point SIMD accumulators to uint8 |
Python's GIL prevents multi-threaded CPU saturation. Solution:
ProcessPoolExecutor.
Never blindly call os.cpu_count() on HPC! Inspect SLURM cgroups:
cores = int(os.environ.get(
"SLURM_CPUS_PER_TASK",
len(os.sched_getaffinity(0))
))
Stop 16 child processes from each spawning 8 threads (128 threads choking 16 cores!):
os.environ["OMP_NUM_THREADS"] = "1"
cv2.setNumThreads(1)
torch.set_num_threads(1)
Batch images into contiguous slices per worker to eliminate 99% of IPC serialization overhead:
# 1000 images / 16 cores
chunk_size = len(images) // workers
make benchmark WORKERS=16 BACKEND=opencv delivers
1,408.9 img/s (2.74x speedup over single-core).
When CPU multi-core hits memory bus bottlenecks, offload to tensor cores.
cv2.imread) streams non-blocking tensors directly to GPU memory (MPS / CUDA).
F.conv2d(batch, weight, padding=1).
(229, 229) executes entirely on high-bandwidth
VRAM via F.interpolate().
Tested across Apple Silicon M5 Max (Metal Performance Shaders) and Google Cloud GPU (Colab Tesla T4).
| Target Platform | Backend | Execution Mode | Wall Time | Throughput | Speedup |
|---|---|---|---|---|---|
| Apple M5 Max (18C) | OpenCV | CPU (1-Core) | 0.290 s | 1,722.9 img/s | 1.00x |
| Apple M5 Max (18C) | OpenCV | CPU (16-Cores) | 0.250 s | 1,996.6 img/s | 1.16x |
| Apple M5 Max (18C) | PyTorch | CPU (16-Cores) | 1.909 s | 261.9 img/s | 0.15x (GIL Trap) |
| Apple M5 Max (Metal) | PyTorch | GPU (MPS) | 0.095 s | 5,278.8 img/s | 5.38x vs CPU |
| Google Cloud (2 vCPUs) | OpenCV | CPU (2-Cores) | 0.994 s | 503.0 img/s | 1.77x |
| Google Cloud T4 (15GB) | PyTorch | GPU (CUDA) | 0.612 s | 817.0 img/s | 6.24x vs CPU |
memray)python -m memray run -o results/profile.bin script.py
python -m memray flamegraph results/profile.bin
scalene)scalene run --cpu-only -o results/profile.json script.py
scalene view results/profile.json
Automate pipeline regression tests with pytest.
Ensure that whether an image is processed via OpenCV, Pillow, or PyTorch, the output strictly matches CNN input dimensions:
def test_opencv_processing():
img = cv2.imread(SAMPLE_IMAGE)
processed = process_opencv(img)
assert processed.shape == (229, 229)
assert processed.dtype == np.uint8
def test_multicore_consistency():
seq = process_all_sequential(images[:10])
par = process_all_parallel(images[:10], workers=4)
assert np.allclose(seq, par)
Automated pre-flight tests catch bugs locally before submitting batch jobs to institutional cluster schedulers:
$ make test
=== test session starts ===
collected 3 items
tests/test_pipeline.py::test_opencv_processing PASSED [ 33%]
tests/test_pipeline.py::test_pillow_processing PASSED [ 66%]
tests/test_pipeline.py::test_pytorch_processing PASSED [100%]
=== 3 passed in 0.42s ===
Submitting your automated pipeline to a SLURM scheduler (sbatch/03_multicore_gpu.sbatch).
#!/bin/bash
#SBATCH --job-name=scientific_pipeline
#SBATCH --output=results/hpc_job_%j.out
#SBATCH --time=00:30:00
#SBATCH --nodes=1 --ntasks=1 --cpus-per-task=16 --mem=16G
#SBATCH --gres=gpu:1 # Optional: request 1 GPU for process-gpu
# 1. Load institutional compiler/environment modules
module purge && module load miniforge # Or: module load Python/3.10
# 2. Execute the idempotent Make DAG across allocated SLURM cpus
make all-uv WORKERS=${SLURM_CPUS_PER_TASK} MAX_IMAGES=5000
sbatch sbatch/03_multicore_gpu.sbatch β’
Inspect status with squeue -u $USER β’ All outputs and flamegraphs will be neatly compiled into results/!
| Task | Make Command | What It Does |
|---|---|---|
| Full UV Pipeline | make all-uv |
Auto-installs uv, creates .venv, downloads data, runs pipeline, tests, and profilers. |
| Full Mamba Pipeline | make all-mamba |
Auto-provisions standalone micromamba, creates .mamba-env, and runs complete workflow. |
| Multi-Core Benchmark | make benchmark WORKERS=8 |
Measures 1-core vs. 8-core scaling speedup and IPC chunking efficiency. |
| Compare All Backends | make benchmark-all |
Runs comparative benchmarks across OpenCV, Pillow, PyTorch CPU, and GPU. |
| GPU Batch Processing | make process-gpu DEVICE=mps(or DEVICE=cuda on Colab/Linux) |
Executes batched 2D tensor convolution and resizing on GPU tensor cores. |
| View CPU Profile | .venv/bin/scalene view results/*.json |
Launches browser GUI to inspect line-by-line native vs. Python CPU bottlenecks. |
| Clean Reset | make clean |
Removes virtualenvs, downloaded images, and profiling artifacts to reset repo. |
Scaling up scientific workflows is a journey of disciplined automation.
Open floor for questions on cluster scaling, profilers, or environment management.