Skip to content

[python_pytorch_profiler] Add new check - #152

Draft
KowalskiThomas wants to merge 1 commit into
mainfrom
kowalski/python_pytorch_profiler-add-new-check
Draft

[python_pytorch_profiler] Add new check#152
KowalskiThomas wants to merge 1 commit into
mainfrom
kowalski/python_pytorch_profiler-add-new-check

Conversation

@KowalskiThomas

Copy link
Copy Markdown
Contributor

What is this PR?

This PR is a correctness check specifically associated to this PR: DataDog/dd-trace-py#18457.

@datadog-official

This comment has been minimized.

@KowalskiThomas
KowalskiThomas force-pushed the kowalski/python_pytorch_profiler-add-new-check branch from 2ab5eed to 1767942 Compare June 4, 2026 09:08
gh-worker-dd-mergequeue-cf854d Bot pushed a commit to DataDog/dd-trace-py that referenced this pull request Jun 12, 2026
## Description

This PR makes our PyTorch integration create profiles that actually look like profiles. Previously, we would get events from the PyTorch Profiler, sample them, and report them without looking at their context. As a result, we would get a "flat flame graph" where every frame would be at the same level.

### Results

**Before my changes** flame graphs are "flat"

<img width="1878" height="123" alt="image" src="https://github.com/user-attachments/assets/000a32c6-fc49-47f1-8f14-82f1e4153987" />

**After my changes** flame graphs are flame graphs (note: different code being profiled)

<img width="1743" height="460" alt="image" src="https://github.com/user-attachments/assets/0bd15ca7-794b-414f-a86c-bc2569ea6ce2" />

[Live example profile](https://ddstaging.datadoghq.com/profiling/explorer?query=service%3Akowalski-python-torch-5&agg_m=count&agg_m_source=base&agg_t=count&color_by=total&fromUser=true&group_by=line&my_code=disabled&refresh_mode=paused&viz=flame_graph&from_ts=1780646782169&to_ts=1780650382169&live=false) (while ~stocks~ retention lasts!)

<details>

The previous flame graph was obtained by profiling the following script.

```py
import os
from time import time

import torch
from torch import nn
from torch.profiler import ProfilerActivity


class FeedForward(nn.Module):
    def __init__(self, d_model: int, d_ff: int) -> None:
        super().__init__()
        self.w1 = nn.Linear(d_model, d_ff)
        self.act = nn.GELU()
        self.w2 = nn.Linear(d_ff, d_model)
        self.norm = nn.LayerNorm(d_model)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        return self.norm(x + self.w2(self.act(self.w1(x))))


class TransformerBlock(nn.Module):
    def __init__(self, d_model: int, n_heads: int, d_ff: int) -> None:
        super().__init__()
        self.attn = nn.MultiheadAttention(d_model, n_heads, batch_first=True)
        self.attn_norm = nn.LayerNorm(d_model)
        self.ff = FeedForward(d_model, d_ff)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        attn_out, _ = self.attn(x, x, x)
        x = self.attn_norm(x + attn_out)
        return self.ff(x)


class DeepModel(nn.Module):
    def __init__(self, d_model: int = 256, n_heads: int = 4, d_ff: int = 512, n_layers: int = 6) -> None:
        super().__init__()
        self.embed = nn.Linear(64, d_model)
        self.blocks = nn.ModuleList([TransformerBlock(d_model, n_heads, d_ff) for _ in range(n_layers)])
        self.head = nn.Sequential(
            nn.Linear(d_model, d_ff),
            nn.GELU(),
            nn.Linear(d_ff, d_model),
            nn.LayerNorm(d_model),
            nn.Linear(d_model, 16),
        )

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        x = self.embed(x)
        for block in self.blocks:
            x = block(x)
        return self.head(x)


def main() -> None:
    torch.set_num_threads(1)

    model = DeepModel()
    x = torch.randn(8, 32, 64)  # (batch, seq_len, input_dim)

    execution_time_sec = float(os.getenv("EXECUTION_TIME_SEC", "30"))
    end = time() + execution_time_sec
    with torch.no_grad():
        while time() < end:
            with torch.profiler.profile(activities=[ProfilerActivity.CPU]):
                for _ in range(10):
                    model(x)


main()
```

</details>

### Performance

Note this theoretically comes at a performance cost, as we need to reconstruct the event parent tree. However, looking at a Python profile for the Profiler (convenient that our Profiler is written in Python!)  
The top lines are
1. `events = prof.events()` -- this line hasn't changed and used to be called as much as it is today.
2. `self_cpu_time: int = e.self_cpu_time_total` -- this is new but is already more than 10x less CPU-intensive than 1.
3. `handle.push_frame(parent.name, _FILE_PLACEHOLDER, 0, 0)` -- this is not new and not called more often than it used to be 
4. `handle.flush_sample()` -- this is not new and not called more often than it used to be 
5. `handle.push_frame(f"PYTORCH_{device_type_str}", _DEVICE_FRAME_FILE_NAME, 0, 0)` -- this is not new and not called more often than it used to be 

In short, the most resource-hungry functions on the PyTorch Profiler at the moment are functions that already were there before, and that already were CPU-hungry before.   
The added logic (to reconstruct the tree) is practically irrelevant as far as performance goes. 


### Related work

New prof-correctness check: DataDog/prof-correctness#152


Co-authored-by: thomas.kowalski <thomas.kowalski@datadoghq.com>
@KowalskiThomas
KowalskiThomas force-pushed the kowalski/python_pytorch_profiler-add-new-check branch from 1767942 to 94feef2 Compare July 7, 2026 08:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant