Skip to the content
FixTheRobot Explore Robotics / UAV / Module 7 / M7-T3

UAV Systems Engineering · Module 7

M7-T3: CPU and GPU workload management

Workloads that are fine alone can fail together. Isolate the time-critical ones, bound the queues, and measure contention under the worst combined load.

  • Lesson 3 of 19
  • Embedded Systems, Timing and System Integration
  • Established engineering
  • About 6 minutes to read

An onboard pipeline is a set of processes competing for cores, memory bandwidth and the GPU. The average load tells you little: the estimator’s deadline is missed in the rare moments when everything peaks together. Managing the workload means isolating what must be predictable and bounding what can grow.

Learning objectives

After this lesson, you should be able to:

  • Assign time-critical tasks to dedicated CPU cores with real-time priority.
  • Bound queues between pipeline stages and choose a drop policy.
  • Explain GPU and memory-bandwidth contention and how to measure it.
  • Report latency with percentiles under combined load.

Conceptual explanation

Established Core affinity, scheduling priorities and queue management are standard Linux and systems practice. Whether isolation features (isolcpus, cgroups, real-time kernels) are available depends on the board support package.

Methods in order of effort:

  • Core affinity: pin the estimator, drivers and sensor-capture threads to specific cores (taskset, sched_setaffinity, or ROS 2 executor thread settings) and keep other work off them (kernel option isolcpus or cgroup cpusets).
  • Priorities: real-time scheduling classes (SCHED_FIFO, SCHED_RR) for time-critical threads, used carefully: a runaway real-time thread can starve the system.
  • Bounded queues: every stage-to-stage queue has a maximum length and a policy (drop oldest, drop newest, block). An unbounded queue turns a short overload into a permanently growing latency.
  • GPU management: a single GPU executes kernels from several processes in time slices or concurrently; a long inference kernel delays another process’s kernels. Use streams and priorities where available, and keep batch sizes consistent.
  • Memory bandwidth: CPU and GPU contend for the same DRAM on shared-memory modules; a bandwidth-heavy stage slows its neighbours even on different cores.
Workload placement and isolationTime-critical estimation runs on pinned CPU cores at real-time priority; perception inference uses the GPU with bounded queues; best-effort tasks share the remaining cores. Contention is measured, not assumed.Pinned coresestimator, driversGPUinference, depthShared coresplanner, mappingBest effortlogging, videoMonitorutilisation, latencyIsolation makes the worst case predictable; measurement shows the cost.
Isolate time-critical work, bound everything else, measure under combined load.
Animation of a camera feeding an inference stage at 30 frames per second. Between 40 and 80 seconds contention slows the stage to below the camera rate. With an unbounded queue the backlog grows, the data age climbs to tens of seconds and stays high long after the spike. With a queue bounded at two frames and a drop-oldest policy the age stays below one hundred milliseconds and some frames are dropped instead. Strips show queue length and data age.
Queueing: when arrivals exceed the service rate the backlog grows linearly, and an unbounded queue never recovers its age within useful time. A bound with drop-oldest caps latency at N/μN/\mu . Fresh and incomplete beats complete and stale for estimation and control. Discrete simulation: 30 fps camera, service time 26 ms rising to 42 ms in the spike. (Looping, about 6 seconds; drawn and computed for this course with illustrative values.)

Engineering intuition

A queue is a promise of delay. If a camera produces 30 frames per second and the detector consumes 25, the queue grows by 5 frames every second; after a minute the detector is looking at images 12 seconds old. With a bound of two frames and a drop-oldest policy the detector sees frames at most about 80 ms old, at the cost of missing some. For state estimation and control that is almost always the right trade: fresh and incomplete beats complete and stale.

The second intuition is that interference is multiplicative at the tail. Two stages each with a p99 latency of 40 ms do not give a p99 of 40 ms together: their occasional peaks coincide more often than you think when they share a resource. So measure the combined pipeline, at the load of the worst mission phase.

Mathematical formulation

For a stage with service rate μ\mu and arrival rate λ\lambda < μ, the M/M/1 queue has mean waiting time Wqq = λ/(μ(μ−λ))\lambda /(\mu (\mu -\lambda )): it diverges as utilisation ρ=λ/μ\rho =\lambda /\mu approaches 1, which is why a 95 % loaded stage is far worse than a 70 % loaded one. For deterministic arrivals and service the queue is empty when λ\lambda < μ and grows linearly when λ > μ\mu :

Queueing delay
Wq=ρμ (1−ρ)(M/M/1)W_q = \frac{\rho}{\mu\,(1-\rho)} \qquad (\text{M/M/1})
overload (λ>μ):backlog grows at (λ−μ) per second\text{overload } (\lambda > \mu):\quad \text{backlog grows at } (\lambda - \mu) \text{ per second}
bounded queue of size N:latency≤Nμ\text{bounded queue of size } N:\quad \text{latency} \le \frac{N}{\mu}

Interference makes service times larger under contention: with a slowdown factor ss the effective service rate is μ/s\mu /s, so a stage at ρ=0.6\rho =0.6 alone is at 0.6 s when another process slows it by s=1.5s=1.5, i.e. 0.9, which is where latency starts to explode. Report p50, p95, p99 and maximum, never only the mean.

λ\lambda , μ\mu
arrival and service rates (1/s).
ρ\rho
utilisation λ/μ\lambda /\mu .
ss
slowdown factor from contention.
NN
queue bound.

Architecture and data flow

Camera driver (pinned core, high priority) → bounded queue (drop oldest) → GPU inference (stream with priority) → bounded queue → tracker or mapper (shared cores) → planner → setpoints. In parallel: IMU driver → estimator (pinned core) → state. Telemetry and logging at low priority on other cores. A monitor records queue depths, drops, per-stage latency and core utilisation to the log.

Real-world UAV applications

  • Keeping VIO real-time while a detector runs on the same module.
  • Choosing core assignments and priorities for a multi-sensor pipeline.
  • Setting queue sizes and drop policies for a camera pipeline.
  • Explaining latency spikes seen in flight logs.

Implementation example

This runs as shown. A discrete-event simulation of a camera → inference pipeline with and without a bounded queue, with the inference stage slowed by contention in the middle of the run. It reports the age of the data reaching the consumer in percentiles.

import numpy as np
from collections import deque

def run(bound, T=120.0, fps=30.0, base_service=0.026, slow_window=(40.0, 80.0), slow=1.6):
    # a single server (inference) fed by a camera at fps; service time rises by 'slow' during the contention window
    t_arr = np.arange(0.0, T, 1 / fps); q = deque(); free_at = 0.0; ages = []; drops = 0; i = 0; now = 0.0
    events = list(t_arr)
    for ta in events:
        # start any services that finish before this arrival
        while q and free_at <= ta:
            tcap = q.popleft(); start = max(free_at, tcap)
            svc = base_service * (slow if slow_window[0] <= start < slow_window[1] else 1.0)
            free_at = start + svc; ages.append(free_at - tcap)           # age of the data when the result is produced
        if bound is not None and len(q) >= bound: q.popleft(); drops += 1   # drop the oldest frame
        q.append(ta)
    return np.array(ages), drops
for name, b in (('unbounded', None), ('bound = 2', 2)):
    ages, drops = run(b)
    print(f"{name:>10}: p50 {np.percentile(ages, 50) * 1e3:7.1f} ms  p99 {np.percentile(ages, 99) * 1e3:8.1f} ms  max {ages.max() * 1e3:8.1f} ms  dropped {drops}")
a_u, _ = run(None); a_b, d_b = run(2)
assert np.percentile(a_u, 99) > 5 * np.percentile(a_b, 99) and a_b.max() < 0.15 and d_b > 0
During contention the unbounded queue grows without limit; the bounded queue with drop-oldest keeps the data age small at the cost of dropped frames.

Pinning a process and giving it real-time priority on Linux (needs privileges; test carefully):

taskset -c 2,3 chrt -f 50 ./estimator_node          # cores 2 and 3, SCHED_FIFO priority 50
cat /proc/cmdline                                       # check for isolcpus= and similar kernel options
ps -eLo pid,tid,class,rtprio,psr,comm | grep estimator  # verify class, priority and the core each thread runs on
Core affinity and real-time priority. A runaway SCHED_FIFO thread can lock up a core: keep a margin and a watchdog.

Common engineering mistakes

Unbounded queues

A short overload becomes permanent latency.

Real-time priority for everything

Priorities only work if few threads use them; otherwise nothing is protected.

Averaging latency

The tail decides deadline misses; use percentiles.

Measuring nodes in isolation

Contention appears only in the combined system at peak load.

Running logging and video on the estimator’s cores

Disk and encoder bursts cause jitter.

Failure modes

Failure modes, symptoms and causes
Failure modeHow it shows upLikely cause
Growing latency over timeFrame age keeps risingArrival rate above service rate with an unbounded queue
Estimator deadline misses at peak loadJitter in state outputShared cores or memory-bandwidth contention
System lockupFrozen companionA real-time thread spinning at high priority
GPU stallsLong gaps between resultsCompeting long kernels from another process

Debugging approach

  1. Plot queue depth, drops and data age against time.
  2. Check per-core utilisation and which threads run where.
  3. Run the pipeline with the worst-case combined load and record percentiles.
  4. Use tracing tools to see scheduling delays of the critical thread.

Practical exercises

  1. Change the bound to 1, 3 and 5 in the example and tabulate p99 age and drops.
  2. Add a second stage with its own bounded queue and measure the end-to-end age.
  3. Measure the slowdown factor ss of your estimator when the detector runs on neighbouring cores.
  4. Define the core assignment table for a four-core companion computer.

Interview questions

Write an answer in your own words, then reveal the model answer. Nothing is sent anywhere.

Question 1

Why bound queues between pipeline stages?

Model answer

An unbounded queue converts a transient overload into an ever-growing delay; a bound with a drop policy caps the age of the data at the cost of missing some, which is the right trade for estimation and control.

Question 2

Why does a stage at 95 % utilisation behave much worse than at 70 %?

Model answer

Queueing delay grows like ρ/(1−ρ)\rho /(1-\rho ), so it rises steeply as utilisation approaches 1, and any slowdown from contention pushes it past the limit.

Question 3

What should you report instead of mean latency?

Model answer

Percentiles (p50, p95, p99) and the maximum under the worst combined load, because deadline misses come from the tail.

References and further reading

Primary papers, official documentation and standards. Each claim above that is specific to a tool or version points here.

What’s next

Mark this lesson

Saved in this browser and synced to your account when you are signed in.

Practice this lesson

M7-L3: Profile perception pipelines on an onboard computer

Profile with tegrastats.

Practice this lesson

M7-L4: Measure CPU and GPU utilization

Measure utilisation.

Next lesson

M7-T4: Real-time operating systems

Explain real-time scheduling, priorities and deadlines, and where PX4 uses the NuttX RTOS versus a Linux companion.