A team hands you a serial physics simulation and a target: get it running on a GPU. The instinct is to reach for the compiler, add some parallel directives, and expect the hardware to do the rest. It rarely works that way. The recompile finishes, the code runs on the GPU, and the speedup is a disappointing 1.4×. The hardware is not the problem. The code was never structured to expose the parallelism the GPU needs. That gap — between what “porting” sounds like and what it actually requires — is where most GPU migration budgets get burned. “Software porting” implies a mechanical translation step: point the toolchain at a new target, resolve the build errors, ship. That framing holds only when the workload’s structure already fits the destination hardware. When it doesn’t, porting stops being translation and becomes restructuring, and the difference between those two categories is the difference between a modest 2× and a move from multi-day runs to hours. What does software porting actually mean in practice? Porting is the work of making a program that runs correctly and acceptably on one hardware or platform target run correctly and acceptably on another. The phrase covers a wide range: moving x86 code to Arm, moving a CPU workload to a GPU, moving native code to WebAssembly, moving a CUDA path to run on non-NVIDIA silicon. What unites them is a change of target; what divides them is how much of the program’s internal structure the new target leaves intact. The naive mental model treats every port as the same kind of task — recompile, fix what breaks, done. That model is correct surprisingly often. If you are moving a well-vectorised numerical library from one x86 generation to another, the compiler does most of the work and the port really is close to a recompile. The model breaks the moment the target has a fundamentally different execution model than the one the code was written for. A GPU is not a faster CPU; it is a throughput machine that wants thousands of independent work items running in lockstep. Serial code has none of that structure to offer. So the useful definition of porting is not “get it building on the new target.” It is: decide whether the algorithm maps to the target as it stands, or whether it has to be redesigned first — and answer that before writing any target-specific code. Those are two genuinely different projects wearing the same word. The difference between recompiling and redesigning Consider the two paths a serial simulation can take to a GPU. The first path is the recompile. You take the existing algorithm, wrap the hot loops in a parallel construct — OpenACC pragmas, a naive CUDA kernel per loop iteration, whatever the toolchain offers — and let it run. If the algorithm has inherent data-parallel structure that was simply expressed serially, this can work well. If it doesn’t — if each step depends on the previous one, if the memory access pattern is scattered, if the work per thread is tiny relative to the launch overhead — you get a GPU that spends most of its time idle or thrashing memory. The recompile succeeds as a build and fails as an optimisation. The second path is the redesign. Before touching a compiler, you ask what the problem looks like when expressed as thousands of independent computations. Often that means changing data layouts from array-of-structs to struct-of-arrays, restructuring loops so that adjacent threads touch adjacent memory (coalesced access), replacing a sequential dependency with a parallel reduction or a scan, or reformulating the numerical method entirely so that the parallel version is not just the serial version with more threads. Only after the algorithm has a parallel shape do you write the target-specific code. These are not two intensities of the same effort. They are different diagnoses. Getting the diagnosis wrong is the expensive mistake, because a redesign misfiled as a recompile produces working code that under-delivers, and everyone spends weeks tuning kernels that were never going to be fast. Recompile vs redesign: a decision table Signal Points toward recompile Points toward redesign Data dependency between steps Independent work items Each step needs the previous result Memory access pattern Regular, contiguous, coalescible Scattered, pointer-chasing, irregular Work per parallel unit Substantial arithmetic per item Tiny work, dominated by launch/sync overhead Existing structure Already loop-vectorised or SIMD-friendly Deeply serial, recursive, or stateful Expected outcome from naive port Meaningful speedup Low single-digit speedup or slowdown Correct next step Port kernels, then tune Restructure algorithm, then port (Evidence class: observed-pattern — these are diagnostic signals drawn from GPU porting engagements, not thresholds from a named benchmark. Treat them as a triage rubric, not a scoring formula.) Why does “parallelise what you have” deliver only modest gains? The phrase “just parallelise it” hides an assumption: that the parallelism is already present and only needs to be exposed. For code written serially over years, that assumption is usually false. Serial algorithms accumulate structure that is actively hostile to parallel execution — global state updated in place, loops whose iteration N reads what iteration N−1 wrote, branch-heavy control flow that causes warp divergence on a GPU. When you parallelise such code as-is, the GPU runs it, but Amdahl’s law and memory behaviour cap the result. A common outcome we see when a serial physics-based algorithm is ported by wrapping its loops without restructuring is a low single-digit speedup — the sort of 1.3× to 2× that makes people conclude “GPUs aren’t worth it for this workload” (observed pattern across porting engagements, not a benchmarked figure). The conclusion is wrong. The workload was worth it; the port was the problem. The same algorithm, redesigned to expose genuine parallelism — reworking the data layout so memory access coalesces, replacing serial accumulation with parallel reductions — can move from a multi-day run to a few hours. The tooling reinforces this trap because it is so accommodating. nvcc will compile a naive kernel without complaint. torch.compile and XLA will graph-optimise what you give them, but they cannot invent parallelism that the algorithm does not contain. The build succeeds, the numbers come back, and nothing in the toolchain tells you that a redesign would have delivered ten times more. This is also why the choice of API — whether you reach for CUDA, or a portable path — is downstream of the redesign question, not a substitute for it. If you are weighing that choice, our comparison of what CUDA and OpenCL each mean in practice when porting AI workloads covers the API-selection layer that sits on top of the algorithmic one. How do you tell which category a workload falls into? The honest answer is that you measure and inspect before you commit. The signals in the decision table above are the first pass. Beyond them, the reliable method is to profile the existing workload and understand where its time actually goes before assuming a GPU will help. A workload dominated by dense linear algebra, image convolutions, or embarrassingly parallel Monte Carlo sampling is a strong recompile-or-light-redesign candidate — the parallel structure is inherent. A workload dominated by sequential state evolution, sparse irregular graph traversal, or fine-grained branching is a redesign candidate, and no amount of compiler flag tuning will change that. The trap is workloads that look parallel at a glance but hide a serial dependency in the inner loop; those are the ones that produce the disappointing recompile. This diagnosis step is exactly what a GPU audit is for. The audit is the porting-triage step: it classifies whether a workload can be ported as-is or requires algorithmic redesign before the target-specific port begins. Doing that classification up front is what avoids paying for GPU hardware that a naive port cannot exploit — the measurable outcome being planning throughput, scenarios evaluated per day rather than per week, once the workload finally runs at the speed the algorithm allows. Profiling discipline matters even on the CPU side of the boundary; the same instinct that drives profiling a Python inference path before a C++ or WASM port applies to GPU triage — measure the real hot path, do not assume it. What kinds of workloads require redesign before a GPU port pays off? Some categories reliably need restructuring rather than translation: Iterative solvers with tight sequential dependencies — Gauss-Seidel-style relaxation, sequential time-stepping where each step reads the full previous state. These often need reformulation (e.g. red-black ordering, Jacobi variants) to expose parallelism. Ray-based or agent-based simulations with irregular work — RF propagation modelling, particle systems with variable interaction counts. The work per unit varies wildly, so naive one-thread-per-item mapping causes severe load imbalance and warp divergence. Graph and tree algorithms with pointer chasing — traversal-heavy code whose memory access is inherently scattered. These need data-structure redesign (structure-of-arrays, sorted adjacency, frontier-based BFS) before the GPU’s memory system can be fed efficiently. Stateful sequential business logic — code where correctness depends on order of execution. Sometimes only a sub-kernel is parallelisable and the rest stays on the host. RF and physics-based planning workloads sit squarely in this space, which is why they are a recurring theme in our telecommunications and GPU engineering work — the redesign-then-port pattern is not a corner case there, it is the norm. Once a workload is correctly classified as redesign-first, the port becomes a genuine engineering project with a predictable payoff, rather than a recompile that quietly disappoints. Server-side redesign is only one porting category. Moving a workload the other direction — onto constrained browser or mobile targets — carries its own profiling-first constraints and a different set of trade-offs, which we treat separately in our work on GPU compile flags and what they change in a CUDA simulation port. FAQ How should you think about software porting in practice? Porting means making a program that runs correctly and acceptably on one target run correctly and acceptably on another. In practice it splits into two very different tasks: a recompile, where the code’s structure already fits the new target and the toolchain does most of the work, and a redesign, where the algorithm must be restructured before any target-specific code is written. Deciding which task you have is the real work. What is the difference between recompiling for a new target and redesigning the algorithm first? A recompile keeps the existing algorithm and re-targets the build, adding parallel constructs where the loops already expose parallelism. A redesign changes the algorithm’s structure — data layouts, dependency patterns, numerical methods — so that a parallel version exists at all, then ports that. A recompile is translation; a redesign is restructuring, and mislabelling one as the other is where GPU budgets get wasted. How do I tell whether a workload can be ported as-is or needs restructuring before porting? Profile the existing workload first and inspect its structure rather than assuming. Independent work items, regular coalescible memory access, and substantial arithmetic per unit point toward a recompile; sequential dependencies, scattered memory access, and tiny per-unit work point toward redesign. Workloads that look parallel but hide a serial dependency in the inner loop are the most dangerous — they produce a working but disappointing recompile. Why does ‘parallelise what you have’ deliver only modest gains when porting serial code to a GPU? Serial code accumulates structure that is hostile to parallel execution — in-place global state, iteration-to-iteration dependencies, branch-heavy control flow. Wrapping its loops in parallel constructs runs on the GPU but is capped by those dependencies and by poor memory behaviour, often yielding a low single-digit speedup. The workload is usually still worth accelerating; it is the naive port, not the hardware, that under-delivers. What role does a GPU audit play in triaging a porting effort before code is written? A GPU audit is the porting-triage step: it classifies whether a workload can be ported as-is or requires algorithmic redesign before the target-specific port begins. Running that classification up front avoids paying for GPU hardware a naive port cannot exploit, and sets a realistic expectation for the payoff. It replaces “recompile and hope” with a diagnosis that tells you which kind of project you actually have. What kinds of workloads require algorithmic redesign before a GPU port pays off? Iterative solvers with tight sequential dependencies, ray- or agent-based simulations with irregular work, graph and tree algorithms with pointer chasing, and stateful sequential business logic all typically need restructuring rather than translation. RF propagation and physics-based planning workloads are common examples. In each case the parallel version is not the serial version with more threads — the data structures or numerical methods have to change first. The question worth carrying into any porting conversation is not “which compiler and API do we use?” but “does this algorithm already have the shape the target hardware wants, or do we have to build that shape first?” Answer that honestly and the port becomes predictable. Skip it, and you find out at the end — after the hardware is bought and the kernels are written — that you were tuning a workload that needed redesigning. That failure class is exactly what a GPU audit exists to catch before the code is written.