# Profiler

Profiler는 학습 및 추론 중에 성능 메트릭을 수집할 수 있는 도구입니다. Profiler의 컨텍스트 매니저 API를 사용하면 가장 비용이 높은 모델 연산자를 파악하고, 해당 연산자의 입력 형식과 스택 트레이스를 검토하며, 디바이스 커널 활동을 분석하고, 실행 트레이스를 시각화할 수 있습니다. 이는 모델의 성능에 대한 통찰력을 제공하여 최적화 및 개선이 가능하도록 합니다.

이 가이드에서는 PyTorch Profiler를 사용하여 모델의 연산자 시간 및 메모리 사용량을 측정하는 방법과 이를 Accelerate에 통합하는 방법을 설명합니다. 다양한 사용 사례를 다루고 각 사례에 대한 예제를 제공합니다.

## 프로파일러를 사용하여 실행 시간 분석

Profiler를 사용하면 프로파일러 컨텍스트 매니저로 감싸진 코드 구간의 실행 중에 어떤 연산자가 호출되었는지 확인할 수 있습니다.

profiler를 사용하여 실행 시간을 분석하는 방법을 살펴보겠습니다:

```python
import torch
import torchvision.models as models
from torch.profiler import profile, record_function, ProfilerActivity

model = models.resnet18()
inputs = torch.randn(5, 3, 224, 224)

with profile(activities=[ProfilerActivity.CPU], record_shapes=True) as prof:
    model(inputs)

print(prof.key_averages().table(sort_by="cpu_time_total", row_limit=10))
```

```python
from accelerate import Accelerator, ProfileKwargs
import torch
import torchvision.models as models

model = models.resnet18()
inputs = torch.randn(5, 3, 224, 224)

profile_kwargs = ProfileKwargs(
    activities=["cpu"],
    record_shapes=True
)

accelerator = Accelerator(cpu=True, kwargs_handlers=[profile_kwargs])
model = accelerator.prepare(model)

with accelerator.profile() as prof:
    with torch.no_grad():
        model(inputs)

print(prof.key_averages().table(sort_by="cpu_time_total", row_limit=10))
```

생성된 테이블 출력(일부 열 생략):

```
---------------------------------  ------------  ------------  ------------  ------------  
                             Name      Self CPU     CPU total  CPU time avg    # of Calls  
---------------------------------  ------------  ------------  ------------  ------------  
                     aten::conv2d     171.000us      52.260ms       2.613ms            20  
                aten::convolution     227.000us      52.089ms       2.604ms            20  
               aten::_convolution     270.000us      51.862ms       2.593ms            20  
         aten::mkldnn_convolution      51.273ms      51.592ms       2.580ms            20  
                 aten::batch_norm     118.000us       7.059ms     352.950us            20  
     aten::_batch_norm_impl_index     315.000us       6.941ms     347.050us            20  
          aten::native_batch_norm       6.305ms       6.599ms     329.950us            20  
                 aten::max_pool2d      40.000us       4.008ms       4.008ms             1  
    aten::max_pool2d_with_indices       3.968ms       3.968ms       3.968ms             1  
                       aten::add_     780.000us     780.000us      27.857us            28  
---------------------------------  ------------  ------------  ------------  ------------  
Self CPU time total: 67.016ms
```

더 세밀한 결과의 세분화 수준을 얻고 연산자 입력 형상을 포함하려면 `group_by_input_shape=True`를 전달하세요(참고: 이 옵션은 프로파일러를 `record_shapes=True`로 실행해야 합니다):

```python
print(prof.key_averages(group_by_input_shape=True).table(sort_by="cpu_time_total", row_limit=10))
```

## 프로파일러를 사용하여 메모리 사용량 분석

Profiler는 모델의 연산자 실행 중에 할당(또는 해제)된 메모리 양(모델의 텐서가 사용하는 메모리)도 표시할 수 있습니다. 메모리 프로파일링 기능을 활성화하려면 `profile_memory=True`를 전달하세요.

```python
model = models.resnet18()
inputs = torch.randn(5, 3, 224, 224)

with profile(activities=[ProfilerActivity.CPU],
        profile_memory=True, record_shapes=True) as prof:
    model(inputs)

print(prof.key_averages().table(sort_by="self_cpu_memory_usage", row_limit=10))
```

```python
model = models.resnet18()
inputs = torch.randn(5, 3, 224, 224)

profile_kwargs = ProfileKwargs(
    activities=["cpu"],
    profile_memory=True,
    record_shapes=True
)

accelerator = Accelerator(cpu=True, kwargs_handlers=[profile_kwargs])
model = accelerator.prepare(model)

with accelerator.profile() as prof:
    model(inputs)

print(prof.key_averages().table(sort_by="self_cpu_memory_usage", row_limit=10))
```

생성된 테이블 출력(일부 열 생략):

```
---------------------------------  ------------  ------------  ------------  
                             Name       CPU Mem  Self CPU Mem    # of Calls  
---------------------------------  ------------  ------------  ------------  
                      aten::empty      94.85 Mb      94.85 Mb           205  
    aten::max_pool2d_with_indices      11.48 Mb      11.48 Mb             1  
                      aten::addmm      19.53 Kb      19.53 Kb             1  
                       aten::mean      10.00 Kb      10.00 Kb             1  
              aten::empty_strided         492 b         492 b             5  
                        aten::cat         240 b         240 b             6  
                        aten::abs         480 b         240 b             4  
              aten::masked_select         120 b         112 b             1  
                         aten::ne          61 b          53 b             3  
                         aten::eq          30 b          30 b             1  
---------------------------------  ------------  ------------  ------------  
Self CPU time total: 69.332ms
```

전체 연산자 테이블 대신 실험 추적에 필요한 집계 숫자 1~2개만 필요하시다면, PyTorch가 노출하는 런타임 메모리 통계와 프로파일러를 조합하세요. 이는 실행당 피크 메모리 사용량을 기록하는 데 일반적으로 충분합니다:

```python
import torch
from accelerate import Accelerator, PartialState, ProfileKwargs

# Resolve the device type/module (cuda, xpu, ...) instead of hardcoding CUDA
device_type = PartialState().device.type
device_module = getattr(torch, device_type)

def summarize_memory_stats():
    return {
        "peak_allocated_mb": device_module.max_memory_allocated() / 1024**2,
        "peak_reserved_mb": device_module.max_memory_reserved() / 1024**2,
    }

profile_kwargs = ProfileKwargs(
    activities=["cpu", device_type],
    profile_memory=True,
    record_shapes=True,
)

accelerator = Accelerator(kwargs_handlers=[profile_kwargs])

device_module.reset_peak_memory_stats()

with accelerator.profile() as prof:
    model(inputs)

print(prof.key_averages().table(sort_by=f"self_{device_type}_memory_usage", row_limit=10))
print(summarize_memory_stats())
```

이 패턴은 프로파일러 출력을 디버깅용으로 유지하면서 실험 트래커에 기록할 수 있는 간결한 값을 생성합니다.

## Chrome trace 내보내기

Chrome trace viewer(`chrome://tracing`)에서 프로파일링된 연산자와 CUDA 커널의 순서를 확인할 수 있습니다:

![profile_export](https://github.com/huggingface/accelerate/assets/100389977/5acb193f-6d11-4f7b-9873-c600c19e8172)

```python
# Pick the available accelerator (cuda, xpu, ...) instead of hardcoding CUDA
device = torch.accelerator.current_accelerator()
device_activity = getattr(ProfilerActivity, device.type.upper())

model = models.resnet18().to(device)
inputs = torch.randn(5, 3, 224, 224).to(device)

with profile(activities=[ProfilerActivity.CPU, device_activity]) as prof:
    model(inputs)

prof.export_chrome_trace("trace.json")
```

```python
model = models.resnet18()
profile_kwargs = ProfileKwargs(
    activities=["cpu", PartialState().device.type],
    output_trace_dir="trace"
)

accelerator = Accelerator(kwargs_handlers=[profile_kwargs])
model = accelerator.prepare(model)
inputs = torch.randn(5, 3, 224, 224).to(accelerator.device)

with accelerator.profile() as prof:
    model(inputs)

# The trace will be saved to the specified directory
```

## Profiler를 사용하여 장기 실행 작업 분석

Profiler는 장시간 실행되는 작업(예: 학습 루프)을 처리하기 위한 추가 API를 제공합니다. 모든 실행을 추적하면 느려질 수 있으며 매우 큰 trace 파일이 생성될 수 있습니다. 이를 피하려면 선택적 인수를 사용하세요:

- `schedule_option`: 스케줄링 옵션을 사용하면 프로파일링이 활성화되는 시점을 제어할 수 있습니다. 이는 데이터 수집량이 과도해지지 않도록 장시간 실행되는 작업에서 유용합니다. 사용 가능한 키는 `wait`, `warmup`, `active`, `repeat`, `skip_first`입니다. 프로파일러는 처음 `skip_first` 스텝을 건너뛰고, `wait` 스텝을 기다린 후, 다음 `warmup` 스텝 동안 워밍업을 수행하고, 다음 `active` 스텝 동안 활성 기록을 수행한 후, `wait` 스텝으로 시작하여 사이클을 반복합니다. 선택적인 사이클 횟수는 `repeat` 파라미터로 지정하며, 0 값은 프로파일링이 완료될 때까지 사이클이 계속됨을 의미합니다.
- `on_trace_ready`: 프로파일러의 참조를 입력으로 받는 함수를 지정하며, 프로파일러가 새로운 trace가 준비될 때마다 이 함수를 호출합니다.

API가 어떻게 작동하는지 설명하기 위해 다음 예시를 살펴보세요:

```python
from torch.profiler import schedule

my_schedule = schedule(
    skip_first=1,
    wait=5,
    warmup=1,
    active=3,
    repeat=2
)

def trace_handler(p):
    output = p.key_averages().table(sort_by=f"self_{device.type}_time_total", row_limit=10)
    print(output)
    p.export_chrome_trace("/tmp/trace_" + str(p.step_num) + ".json")

with profile(
    activities=[ProfilerActivity.CPU, device_activity],
    schedule=my_schedule,
    on_trace_ready=trace_handler
) as p:
    for idx in range(8):
        model(inputs)
        p.step()
```

```python
device_type = PartialState().device.type

def trace_handler(p):
    output = p.key_averages().table(sort_by=f"self_{device_type}_time_total", row_limit=10)
    print(output)
    p.export_chrome_trace("/tmp/trace_" + str(p.step_num) + ".json")

profile_kwargs = ProfileKwargs(
    activities=["cpu", device_type],
    schedule_option={"wait": 5, "warmup": 1, "active": 3, "repeat": 2, "skip_first": 1},
    on_trace_ready=trace_handler
)

accelerator = Accelerator(kwargs_handlers=[profile_kwargs])
model = accelerator.prepare(model)

with accelerator.profile() as prof:
    for idx in range(8):
        model(inputs)
        prof.step()
```

## FLOPS

formula를 사용하여 특정 연산자(행렬 곱셈 및 2D 컨볼루션)의 FLOPs(부동소수점 연산)를 추정합니다.

부동소수점 연산(FLOPS)을 측정하려면:

```python
with profile(
    activities=[ProfilerActivity.CPU, device_activity],
    with_flops=True
) as prof:
    model(inputs)

print(prof.key_averages().table(sort_by="flops", row_limit=10))
```

```python
profile_kwargs = ProfileKwargs(
    with_flops=True
)
accelerator = Accelerator(kwargs_handlers=[profile_kwargs])

with accelerator.profile() as prof:
    model(inputs)

print(prof.key_averages().table(sort_by="flops", row_limit=10))
```

생성된 테이블 출력(일부 열 생략):

```
-------------------------------------------------------  ------------  ------------  ------------  
                                                   Name      Self CPU     Self CUDA    Total FLOPs  
-------------------------------------------------------  ------------  ------------  ------------  
                                           aten::conv2d     197.000us       0.000us  18135613440.000  
                                            aten::addmm     103.000us      17.000us     5120000.000  
                                              aten::mul      29.000us       2.000us          30.000  
                                      aten::convolution     409.000us       0.000us            --  
                                     aten::_convolution     253.000us       0.000us            --  
                                aten::cudnn_convolution       5.465ms       2.970ms            --  
                                        cudaEventRecord     138.000us       0.000us            --  
                                  cudaStreamIsCapturing      43.000us       0.000us            --  
                                  cudaStreamGetPriority      40.000us       0.000us            --  
                       cudaDeviceGetStreamPriorityRange      10.000us       0.000us            --  
-------------------------------------------------------  ------------  ------------  ------------  
Self CPU time total: 21.938ms
Self CUDA time total: 4.165ms
```

## 결론 및 추가 정보

PyTorch Profiler은 모델의 성능을 분석하는 강력한 도구입니다. Accelerate와 통합하면 모델을 쉽게 프로파일링하여 성능에 대한 통찰력을 얻을 수 있으며, 이를 통해 모델을 최적화하고 개선하는 데 도움이 됩니다.

자세한 정보는 [PyTorch Profiler 문서](https://pytorch.org/docs/stable/profiler.html)를 참조하세요.

