Enable unified virtual memory, drop the explicit cudaMemcpy calls, and expect the same throughput with less code. That is the assumption most teams carry into their first UVM port, and it is the one that quietly costs them tail latency. Unified Virtual Memory (UVM) does give you a single pointer address space shared between host and device — but it does not make the data movement disappear. It moves the when and the who: instead of you copying data at a known point in your code, the CUDA driver services the transfer on demand, as a page fault, at whatever moment a kernel happens to touch a page that is not yet resident on the device. For a batch job that streams once through a large dataset, that trade is often fine and occasionally elegant. For a hot inference loop that runs the same weights against a stream of requests under a latency target, it can be a quiet regression that only shows up in the p99. The honest version of “UVM is simpler” is “UVM is simpler and it relocates a cost you used to control.” Whether that relocation helps or hurts depends entirely on your access pattern, and the only way to know is to profile the migration traffic — not to reason about it from the API surface. What Unified Virtual Memory Actually Gives You UVM, introduced in CUDA 6 and matured with the page-fault engine on Pascal and later architectures, lets you allocate with cudaMallocManaged and hand the same pointer to both host code and device kernels. The runtime maintains a coherent view of that allocation across the CPU and GPU physical memories. When the CPU writes to a managed page and the GPU later reads it, the page migrates to device memory; when the CPU reads a page the GPU last wrote, it migrates back. The single-address-space part is real and useful. It removes an entire class of pointer-bookkeeping bugs — the ones where you free a host buffer while a device pointer still aliases it, or copy the wrong direction, or forget to copy at all. It makes pointer-rich data structures (trees, graphs, linked lists) usable across the host/device boundary without hand-serialising them, which is exactly the case where explicit copies are painful to write and easy to get wrong. What UVM does not do is repeal the physics of the interconnect. On a discrete GPU, host and device memory sit on opposite sides of a PCIe or NVLink link, and moving a page across that link takes real time and real bandwidth. UVM does not have a faster path to the device than cudaMemcpy does — it uses the same DMA engines over the same link. It simply chooses the timing for you, reactively, when a fault fires. Understanding that the transfer still happens is the whole point: this is the difference between build/deploy engineering, which is our territory, and the broader question of how a workload moves onto new hardware at all. Does UVM Eliminate Host-to-Device Copies, or Just Defer Them? It defers them. This is the single most important thing to internalise, and it is the point where the naive mental model breaks. With explicit memory management, you decide when the transfer happens. You can issue an asynchronous cudaMemcpyAsync on a copy stream while a previous batch computes, overlapping the transfer with useful work so the copy latency never appears on the critical path. That overlap is the mechanism behind well-pipelined inference: the data for batch N+1 is in flight while batch N is on the tensor cores. With UVM’s on-demand paging, the transfer is triggered by a page fault the instant a kernel dereferences an unmigrated address. The kernel stalls at that point. The GPU raises a fault, the driver walks the page tables, schedules the migration, waits for the DMA to complete, and only then lets the warp continue. If that fault lands in the middle of a compute-bound kernel that expected its inputs to be resident, you have converted a copy you could have hidden into a stall you cannot. The transfer did not vanish — it relocated onto your critical path and lost the overlap you would have engineered by hand. There is a second, subtler cost: fault granularity and thrashing. UVM migrates at page granularity, and a sparse or strided access pattern can trigger many small faults instead of one large streamed copy. If both the CPU and GPU touch the same allocation in an interleaved way, pages can ping-pong back and forth across the link, burning migration bandwidth with no net progress. That is the pathological case, and it does not announce itself in a functional test — the code produces correct results, just slowly and unpredictably. When UVM Fits an Inference Workload — and When It Does Not UVM earns its place in a bounded set of situations, and the decision is workload-shaped rather than a matter of taste. The table below is the frame we use when deciding whether to leave an inference path on explicit copies or move it to managed memory. Situation Prefer explicit cudaMemcpy / prefetch Prefer UVM Hot loop with a strict latency target ✅ staged, overlapped copies keep transfers off the critical path ❌ page faults add unpredictable tail latency Working set fits resident in device memory ✅ copy once, reuse ➖ no benefit; adds fault overhead Working set larger than device memory ➖ manual tiling is complex to write ✅ driver pages in/out on demand Sparse / irregular access over a large structure ➖ hard to predict what to copy ✅ migrate only touched pages Pointer-rich data structures crossing host/device ➖ manual serialisation is error-prone ✅ single address space removes the bookkeeping Developer time is the binding constraint ➖ ✅ fewer lines, fewer copy-direction bugs Throughput matters to within a few percent ✅ predictable, tunable ➖ migration cost is workload-dependent The dividing line is predictability. A production inference loop usually wants staged, overlapped transfers because it needs its latency distribution to be tight and repeatable — the exact profile you get by knowing precisely where the cost lives in an inference path and moving it off the hot section deliberately. UVM shines when the access pattern is too sparse or too large to stage by hand, and when a few percent of throughput is a fair price for cutting the memory-management code by half. What Is the Latency and Bandwidth Cost of On-Demand Migration? The cost has three components, and each is measurable. The first is the per-fault service latency: the fixed overhead of raising the fault, walking the page tables, and scheduling the migration, paid once per fault regardless of size. The second is the transfer time itself, which scales with migration bytes and the effective link bandwidth — the same DMA cost an explicit copy would pay, but incurred at a moment you did not choose. The third is the stall propagation: because a faulting warp blocks, the fault latency can serialise behind compute you expected to run concurrently. We frame these as an illustrative example rather than a fixed figure, because the numbers are entirely a function of your hardware and access pattern (observed pattern across port assessments; not a published benchmark). If a single inference triggers, say, a few hundred small page faults that in aggregate move a modest slice of a model’s activations, the fixed per-fault overhead can dominate — many small faults are far more expensive than one streamed copy of the same total bytes. If instead the model touches a large contiguous region once, the fault count is low and the cost approaches that of a plain copy. The point is not the specific millisecond figure; it is that the shape of the cost follows the shape of your access, and you cannot read it off the spec sheet. This is why page-fault-driven tail latency and migration bandwidth belong in your release-readiness signals for any CUDA inference path before it ships — the same discipline that governs how a heterogeneous CPU/GPU/WASM inference architecture divides its work. A path that meets its median latency target but blows past p99 under UVM paging is not ready, and the gap will be invisible until you measure it. How Do You Profile UVM Page Faults Against a Latency Target? Treat it as a measurement pass, not a code review. The migration behaviour is a runtime property; you cannot infer it from reading the source. A practical profiling checklist: Count the page faults per inference. Nsight Systems surfaces UVM page-fault events on the timeline, and nvprof/the CUDA profiling APIs expose managed-memory fault and migration counters. A high fault count per request is the first warning sign. Measure migration bytes per inference. Compare host-to-device and device-to-host migration volume against the size of the data you actually expect to move. If migration bytes exceed the working set, you have thrashing. Isolate the tail. Run the loop under sustained load, not one-shot, and read the p95/p99 latency — not the mean. On-demand paging inflates the tail long before it moves the median. Diff against a baseline. Build the same path with explicit cudaMemcpyAsync and prefetch, and compare per-inference latency delta directly. The number you want is “how much tail latency did UVM cost me,” expressed against your target. Document the decision. Record the fault count, migration bandwidth consumed, and the latency delta, then write down whether UVM’s simpler memory model is worth its runtime cost for this workload. That artifact is what turns a hunch into a defensible port decision. That documented baseline is exactly the memory-movement figure our Inference Cost-Cut Pack uses when a port-assessment pass compares a native C++/CUDA path against alternatives. And it connects to a decision that reaches beyond CUDA entirely, which the next section addresses. Why UVM Matters to a CUDA-vs-WASM Port Decision Here is where the concept stops being a CUDA implementation detail and becomes a portability question. When you evaluate moving an inference path off native CUDA — say, to a WASM/Pyodide target that runs in a browser or a sandboxed runtime — there is no unified virtual memory to inherit, because there is no native CUDA memory model at all. WASM has a linear memory heap and, at best, a WebGPU or WebGL compute path with its own explicit buffer-upload semantics. The elegant single-address-space convenience UVM gave you does not travel. That matters for the baseline. If your native path leans on UVM to handle a sparse, pointer-rich access pattern, the “cost” you are really carrying is the migration traffic UVM hides — and any non-CUDA target has to reproduce that data movement explicitly, or restructure the algorithm to avoid it. A port assessment that measured UVM’s migration bytes has already quantified the data-movement problem the WASM path must solve; a port assessment that never profiled it is comparing a known CUDA path against an unknown one. This is the concrete link between a memory-model explainer and a real inference cost-cutting engagement on the GPU practice: the UVM profile is not trivia, it is the input to the comparison. Do Prefetch Hints and Memory Advises Change the Picture? They can, and this is the middle path most teams overlook. UVM is not strictly reactive — CUDA gives you cudaMemPrefetchAsync to migrate a managed region to a destination before a kernel needs it, and cudaMemAdvise to hint access patterns (SetReadMostly, SetPreferredLocation, SetAccessedBy) so the driver makes smarter migration and mapping decisions. Used well, prefetch reclaims most of the overlap you would have engineered with explicit copies, while keeping the single-address-space convenience. You keep the managed pointers and the clean code, but you tell the runtime “move this region to the device now, on this stream,” so the transfer happens ahead of the fault instead of on top of it. cudaMemAdvise with SetReadMostly lets the driver keep read-only weights resident on both sides without ping-ponging. The catch is that once you are placing prefetch hints and advises throughout your inference loop, you are doing manual memory management again — just with a different API surface and, arguably, less clarity about what actually moves and when. There is a real question of whether hint-laden UVM code is simpler than clean explicit copies, or merely differently complex. Our answer, in practice: prefetch is worth it when it lets you keep UVM for the structure (pointer-rich, sparse, oversized working sets) while removing UVM’s latency penalty on the hot region. It is not worth it as a reflex on a path that would have been three lines of cudaMemcpyAsync. FAQ How does unified virtual memory work in practice? UVM gives the CPU and GPU a single shared pointer address space via cudaMallocManaged, and the CUDA driver keeps that memory coherent across host and device by migrating pages between them as they are accessed. In practice it means you can hand the same pointer to host code and device kernels without writing explicit copies. The convenience is real — it removes a class of pointer-bookkeeping bugs — but the driver services the underlying data movement on demand at runtime rather than eliminating it. Does UVM actually eliminate host-to-device copies, or just defer them into runtime page faults and migrations? It defers them. UVM uses the same DMA engines over the same PCIe or NVLink link that cudaMemcpy uses; it does not have a faster path to the device. Instead of you choosing when the transfer happens, the driver triggers it as a page fault the moment a kernel touches an unmigrated page, which stalls the kernel until the migration completes. The copy did not disappear — it relocated onto your critical path and lost any overlap you would have engineered by hand. When does UVM fit an inference workload better than explicit cudaMemcpy or staged prefetch? UVM fits when access patterns are sparse, irregular, or too large to fit resident in device memory, and when developer simplicity outweighs a few percent of throughput. It also shines for pointer-rich data structures that would be painful to serialise for explicit copies. It does not fit a hot inference loop with a strict latency target, where staged, overlapped copies keep transfers off the critical path and give a predictable latency distribution. What is the latency and bandwidth cost of on-demand page migration in a hot inference loop? The cost has three parts: a fixed per-fault service latency, the transfer time itself which scales with migration bytes, and stall propagation because a faulting warp blocks execution. Many small faults are far more expensive than one streamed copy of the same total bytes, so a sparse access pattern can make the fixed overhead dominate. The exact figures depend entirely on your hardware and access pattern, which is why they must be measured rather than read off a spec sheet. How do you profile UVM page faults and migration traffic against a latency target before committing? Run the inference loop under sustained load and use Nsight Systems and the CUDA profiling counters to count page faults per inference and measure migration bytes per inference. Read the p95/p99 latency, not the mean, because on-demand paging inflates the tail before it moves the median. Diff those numbers against an explicit cudaMemcpyAsync-plus-prefetch baseline to get a per-inference latency delta, then document whether UVM’s simpler model is worth that runtime cost for the workload. Why does UVM matter to a CUDA-vs-WASM port decision when the WASM/Pyodide path has no native CUDA memory model at all? A WASM/Pyodide target has a linear memory heap and, at best, a WebGPU compute path with its own explicit buffer semantics — there is no unified virtual memory to inherit. If a native path leans on UVM to handle sparse or pointer-rich access, the migration traffic UVM hides is exactly the data-movement problem a non-CUDA target must reproduce explicitly or restructure away. A port assessment that measured UVM’s migration bytes has already quantified that problem; one that never profiled it is comparing a known CUDA path against an unknown one. How do prefetch hints and memory advises change UVM behaviour, and when are they worth the added code? cudaMemPrefetchAsync migrates a managed region to the device before a kernel needs it, and cudaMemAdvise hints (like SetReadMostly or SetPreferredLocation) let the driver make smarter migration decisions. Used well, prefetch reclaims most of the overlap you would have engineered with explicit copies while keeping the clean single-address-space code. It is worth the added code when it lets you keep UVM for structure — sparse, pointer-rich, or oversized working sets — while removing its latency penalty on the hot region, and not worth it as a reflex on a path that would have been three lines of cudaMemcpyAsync. Where this leaves the reader is with a question, not a rule: before you assume UVM is free, can you show the migration profile of your hottest inference path against its latency target? If the closest reference point you have is how unified virtual memory shapes an XR rendering budget, you already know the answer changes with the workload — and that the page-fault trace, not the API, is what decides whether managed memory belongs on your critical path.