Home · ASPLOS 2025 · Machine Learning Systems

DeepContext: A Context-aware, Cross-platform, and Cross-framework Tool for Performance Profiling and Analysis of Deep Learning Workloads

Qidong Zhao, Hao Wu, Yueming Hao, Zilingfeng Ye, Jiajia Li, Xu Liu and 1 other

Conference
ASPLOS 2025
Published
2025
Pages
48-63
Citations
2

When a deep-learning program runs slowly, the code you wrote is Python, but the time is actually spent far below it — inside the framework, in C++ libraries, and in GPU kernels — and today's profilers show you the low-level pieces without telling you which line of your model triggered them; this paper (NC State and George Mason) builds DeepContext, which stitches together one call path spanning Python, the framework, C/C++, and the GPU (down to individual instructions), attributes hardware metrics to it, and then automatically suggests fixes like fusing kernels — letting the authors find hidden bottlenecks and speed real workloads up by 1.06 to 1.66 times.

The argument in one glance

  1. Problem

    Your code is Python, but the time is spent four layers below it

    A modern deep-learning program is a thin layer of Python sitting on top of a deep stack: the Python you wrote calls into a framework like PyTorch or JAX, which calls into C/C++ libraries, which launch kernels that run on the GPU, where the real time and energy are spent. When such a program is slow, the developer needs to know which line of their model caused which expensive piece of hardware work. But the layers speak different languages and run on different threads and devices, so the connection between a costly GPU kernel and the Python operation that triggered it is lost. Optimizing the workload means bridging that semantic gap — matching low-level execution back to high-level intent — and today that bridge has to be built by hand, from fragments, for every performance question.

  2. What people did

    Trace every operation and lay it out on a timeline

    Framework profilers (the PyTorch and JAX profilers) and vendor tools (Nsight Systems, RocTracer, VTune) trace individual CPU and GPU operations, capturing per-operation metrics and displaying them on a timeline the developer scrolls through to hunt for bottlenecks.

  3. Why it fell short

    No tool connects the whole stack, and traces explode

    These tools each see only part of the stack. At best Nsight Systems can correlate Python with C++, but it cannot tie in deep-learning operators or the GPU instructions inside a kernel; framework profilers see their own operators but not the native call path or intra-kernel detail. So a hot kernel appears with no path back to the Python that launched it — worse still because forward and backward operators run on different CPU threads that lose each other context. And tracing every operation produces enormous profiles for long-running training, burying the issue in data and burning memory and disk.

  4. This paper's idea

    Assemble one call path across the whole stack, attribute metrics to it, and advise

    DeepContext builds a single call path that spans Python, the framework, C/C++, the GPU kernel, and even individual GPU instructions, using a shim layer (DLMonitor) that intercepts framework and GPU operations and splices together call paths from CPython, libunwind, and a per-thread shadow stack. It attributes hardware metrics to that unified context in a calling context tree, aggregating online to keep profiles small, and an automated analyzer then reads the holistic view and suggests concrete fixes — like fusing small kernels — that yield 1.06 to 1.66 times speedups.

Why this problem is worth solving

The reason performance is so hard to reason about here is that a profiler is trying to explain the behavior of code written at one level in terms of work done at a very different level.

The semantic gap is the disconnect between the high-level operation a developer expresses and the low-level execution the hardware performs. A developer writes, in Python, something like a convolution or an attention layer; by the time that runs, it has passed through the framework’s dispatcher, into templated C++ kernels, and out as one or several GPU kernels made of thousands of machine instructions, possibly on a thread the developer never created. A profiler that reports only the low level — this GPU kernel took so many microseconds, this instruction stalled — tells the developer what happened but not why, because it cannot say which line of their model, through which framework operator, produced that kernel. Bridging the gap means attributing every low-level cost back to the high-level context that caused it, which is exactly the information a developer needs to know what to change and is exactly what per-operation timeline tracing throws away.

The right structure for holding that attribution is one profiling has used for decades.

A calling context tree is a data structure that records performance metrics not against individual functions but against full call paths — the chain of callers that led to each point. The root is the program entry; each path from the root to a node is a distinct calling context, so the same function called from two places appears as two nodes with separate metrics. This is powerful for attribution because a cost is charged to the exact context that incurred it: not just this kernel is hot, but this kernel, launched by this operator, from this Python line, is hot. DeepContext builds a calling context tree whose paths run all the way from Python down into GPU instructions, and aggregates each metric up the path to the root as it is collected. Because metrics are folded into the tree online rather than logged per event, the profile stays compact even for long training runs, which is what lets context-rich profiling scale where timeline tracing drowns in data.

What people did before, and where it broke

  • Framework profilers (PyTorch profiler, JAX profiler). Instrument the framework to record each operator and its device time. Where it broke. They see their own operators but not the native C/C++ call path, not the Python calling context above cleanly, and not the instructions inside a GPU kernel; and they store per-operation traces that balloon in size and memory on long runs.
  • Vendor system tracers (Nsight Systems, RocTracer, VTune). Capture CPU and GPU activity system-wide on a timeline. Where it broke. They lack framework semantics — a kernel shows up with no deep-learning operator or Python line attached — and at best correlate Python with C++ without reaching operators or intra-kernel detail, leaving the developer to guess the connection.
  • Native call-path profilers (HPCToolkit, TAU, perf, DrCCTProf). Provide rich calling-context profiling for low-level C/C++ and the operating system. Where it broke. They know nothing about Python or deep-learning frameworks, so they profile the native layer in isolation and cannot express a bottleneck in terms a model author would recognize.
  • Trace post-processors and timeline analyzers. Take the traces existing tools emit and analyze the patterns to give coarse recommendations. Where it broke. They inherit the blind spots of the traces they consume, reason at a coarse grain, and do not combine fine-grained hardware metrics with high-level program context, so their advice stays generic.
  • The gap. No tool produced a single calling context that spanned every layer of the deep-learning stack, from Python through the framework and C/C++ down to GPU kernels and their instructions, attributed real hardware metrics to it, and then turned that holistic view into concrete, actionable optimization advice — all at overhead comparable to existing profilers.

How DeepContext is put together

From intercepted operations to a whole-stack calling context tree with advice

  1. new

  2. new

  3. new

  4. new

  5. new

The idea

Stitch the layers into one call path instead of profiling each in isolation. The reason existing tools leave a semantic gap is that each one lives at a single altitude: framework profilers see operators, native profilers see C/C++, vendor tracers see the timeline, and none of them produces a path that runs the whole height of the stack. DeepContext core move is to construct that path. At any interception point it pulls the Python frames from CPython, the native frames from libunwind, and the framework operators from a shadow stack it maintains per thread, and then merges them by walking the native path from the bottom up: where a frame address matches a recorded operator it inserts the operator name, where a frame falls inside the Python interpreter it substitutes the Python call path, and at a GPU kernel launch it appends the kernel, extending even into the kernel by adding the program counter of each sampled instruction. The result is a single context from a Python line down to a GPU instruction. Attributing low-level hardware costs to the full high-level calling context that produced them, rather than reporting each layer alone, is the same context-attribution philosophy the fine-grained value-profiling framework covered elsewhere on this site is built on, charging every measured event to the precise call path responsible so a developer sees cause, not just symptom.

Put a framework-agnostic shim between the profiler and the framework. Bridging the stack would be hopeless if it required bespoke integration with each framework internals, so DeepContext isolates that work behind DLMonitor, a shim exposing one small interface: initialize, register a callback on a domain, get the current multi-layer call path, finalize. A profiler written against this interface works across frameworks even though the mechanics differ underneath — PyTorch is intercepted through its built-in global-operator-callback hook in eager mode, while JAX, which fuses operators into compiled graphs and has no per-operator callback, is handled by lightweight binary instrumentation of its compilation passes plus a recorded mapping from fused operators back to the originals so the source context survives fusion. It reaches hardware uniformly too, through CUPTI on Nvidia and RocTracer on AMD, and even accommodates devices without vendor callbacks by letting a user declare the driver functions to hook in a config file. Hiding the messy, per-framework and per-vendor interception behind one uniform operation-intercepting interface is the same discipline the unified deep-learning instrumentation work covered elsewhere on this site relies on, giving tools a single way to attach to many frameworks instead of rewriting the tool for each one.

Do not just show the profile — read it and prescribe the fix. A whole-stack profile is only useful if a developer can act on it, so DeepContext pairs the calling context tree with an automated analyzer that encodes expert rules over the combined fine- and coarse-grained metrics and the program context. It searches the tree for semantic patterns — hotspots, a frame that launches many tiny kernels, GPU instruction stalls, frames whose CPU time dwarfs their GPU time — and flags each with a concrete suggestion, such as fusing several small kernels that share a parent, then surfaces it in a flame-graph GUI that links straight to the metrics and the source line. This is what converts the profile into edits: in the paper own use cases it points to fusing softmax, copy, and loss kernels, to fixing a wasteful memory-layout conversion, and to swapping a serializing operator for an atomic one. Turning a rich performance analysis into specific, mechanism-level optimization actions rather than leaving the developer to interpret raw numbers is the same aim the kernel-orchestration work covered elsewhere on this site pursues, deciding concretely how to fuse and schedule a tensor program operators for speed instead of merely reporting that it is slow.

Following a slow training step through DeepContext

Attributing a hot GPU kernel back to its Python line

A trace, drawn from the paper's design and use cases, of how DeepContext connects an expensive kernel to the model code that caused it and suggests a fix.

Step 1 of 5

DLMonitor intercepts the operator and captures context

As the training step runs, DLMonitor callbacks fire before and after each deep-learning operator. At each, it can capture the operator inputs and outputs and, on request, assemble the current call path spanning the Python line, the framework operator, and the native C/C++ frames beneath it.

For PyTorch this uses the aten global-callback interface; the framework layer of the path comes from a per-thread shadow stack DLMonitor updates as operators are entered and exited.

Why it works

  • A single call path restores the cause that timeline tracing loses. Per-operation traces show a hot kernel with no link to the Python that launched it → but splicing Python, framework, native, and GPU frames into one path attributes the cost to its full context → so a developer sees which model line, through which operator, produced the expensive kernel.
  • A framework-agnostic shim makes one profiler span many frameworks. Integrating a profiler with each framework internals is bespoke and brittle → but routing all interception through DLMonitor uniform interface, with PyTorch hooks and JAX binary instrumentation behind it → so the same tool profiles PyTorch and JAX, on Nvidia and AMD, without source changes.
  • Online aggregation into a tree keeps rich profiling scalable. Logging every operation explodes profile size and memory on long training runs → but folding metrics into a calling context tree as they arrive keeps only the aggregated context → so DeepContext memory overhead stays around 1.00 to 2.44 times where trace tools reach up to 27 times.
  • Cross-thread stitching recovers backward context others drop. Backward operators run on separate threads whose native path has no Python source → but PyTorch shared forward-backward sequence ID lets DeepContext fetch and reattach the forward context → so even generically-named backward kernels are traceable to the model code responsible.
  • Reading the profile with rules turns data into edits. A raw whole-stack profile still leaves the developer to interpret it → but rules over combined fine- and coarse-grained metrics and context flag specific issues with concrete suggestions → so the analysis converts directly into fixes that delivered 1.06 to 1.66 times speedups on real workloads.

What it costs (trade-offs and scope)

  • Assembling context is not free, and cost varies by workload. Median running-time overhead is about 1.12 times for PyTorch on Nvidia and 1.50 times on AMD, and about 1.33 and 1.28 times for JAX; collecting the native C/C++ call path pushes it higher (to roughly 1.50 to 1.90 times), and large models like Llama 3 and Gemma-7B saw notably higher overhead from frequent unwinding. The tool lets users drop specific call-path sources to trade detail for speed.
  • The unified path relies on framework-specific hooks and internals. PyTorch is intercepted through its global-callback interface and its forward-backward sequence IDs; JAX needs binary instrumentation of its compilation passes and a fused-to-original operator mapping. These lean on current framework mechanisms, and supporting a new framework or a hardware backend without vendor callbacks requires new interception (the latter via a user-written config of driver signatures).
  • Fine-grained metrics depend on vendor sampling facilities. Intra-kernel instruction stalls come from Nvidia and AMD instruction-sampling APIs, and metric collection goes through CUPTI and RocTracer; the depth of insight available is bounded by what those interfaces expose on a given GPU, and mapping instructions back to source relies on DWARF debug information being present.
  • The analyzer is expert rules, not automatic discovery. The optimization suggestions come from human-defined analyses (hotspot, kernel fusion, stall, CPU latency); they cover common patterns well and are extensible through a Python interface, but they will not surface an issue no rule anticipates, and applying a suggested fix (for example, actually fusing kernels) remains a manual code change.
  • Evidence is use-case driven on a set of workloads. The reported 1.06 to 1.66 times speedups come from seven use cases on models such as ResNet, U-Net, Transformer-Big, a GNN, Llama 3, Gemma, and nanoGPT; they convincingly demonstrate that the tool finds real, fixable bottlenecks, but they are targeted case studies rather than a broad automated study across many applications.

The evaluation

Three approaches to profiling a deep-learning workload

PropertyFramework profilerVendor system tracerDeepContext
How far up and down the stack it sees
Attributes a hot kernel to the Python line
Memory overhead on long runs
Runtime overhead
Turns the profile into a fix
DeepContext against framework profilers and vendor tracers, from the paper. Overheads are medians across workloads (ResNet, U-Net, Transformer-Big, a GNN, Llama 3, Gemma, nanoGPT) on Nvidia and AMD GPUs; acting on the analyzer's suggestions yielded end-to-end speedups of 1.06 to 1.66 times.Underlined values explain themselves when clicked.

Check yourself

Check your understanding

  1. 1. What is the core problem DeepContext addresses?

  2. 2. How does DeepContext build a call path that spans the whole stack?

  3. 3. Why does DeepContext use a calling context tree with online aggregation instead of logging every operation?

  4. 4. Beyond collecting data, what does DeepContext do to help optimization, and what did it achieve?

Sources

The paper is “DeepContext: A Context-aware, Cross-platform, and Cross-framework Tool for Performance Profiling and Analysis of Deep Learning Workloads” by Qidong Zhao, Hao Wu, Yueming Hao, Zilingfeng Ye, Jiajia Li, Xu Liu, and Keren Zhou, from North Carolina State University and George Mason University, published at ASPLOS 2025, DOI 10.1145/3676642.3736127.

This explainer is written from the full text of the authors’ arXiv version (arXiv:2411.02797). Everything above comes from the paper’s own sections: the introduction and Table 1 comparison framing the semantic-gap and trace-explosion limitations of framework profilers (PyTorch, JAX), vendor tracers (Nsight Systems, RocTracer, VTune), and native call-path profilers (HPCToolkit, TAU, perf, DrCCTProf); the four-component architecture (DLMonitor shim, profiler, analyzer, GUI); the DLMonitor design (its init/register/get/finalize APIs, LD_PRELOAD loading, interception of PyTorch via the aten global-callback interface and of JAX via binary instrumentation of its compilation passes with a fused-to-original operator mapping, and LD_AUDIT-based custom callbacks for callback-less hardware); the call-path integration from CPython frames, libunwind native frames, and a per-thread framework shadow stack, extended by kernel name and per-instruction program counters; the profiler’s use of CUPTI and RocTracer for GPU metrics and Perf events and PAPI for CPU metrics, correlation-ID linking, and the calling context tree with its frame-collapsing and metric-aggregation rules; the optimizations (forward-backward association via PyTorch sequence IDs, and thread-local call-path caching); the analyzer’s three dimensions and its example analyses (hotspot, kernel fusion, fine-grained stall, CPU latency) with their pseudocode; and the evaluation, including the median runtime overheads (about 1.12 and 1.50 times for PyTorch, 1.33 and 1.28 times for JAX on Nvidia and AMD), the memory-overhead comparison (1.00 to 2.44 times versus 1.29 to 27.28 times for framework profilers), and the seven use cases on ResNet, U-Net, Transformer-Big, a GNN, Llama 3, Gemma, and nanoGPT that yielded end-to-end speedups of 1.06 to 1.66 times (including the U-Net 54-to-42-second and the 73.2-to-44.0-second operator results).

Related papers on Machine Learning Systems

See all 212 →