Files
paddlepaddle--paddle/python/paddle/device/cuda/memory_analyzer.py
T
2026-07-13 12:40:42 +08:00

541 lines
17 KiB
Python

# Copyright (c) 2025 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import numpy as np
import paddle
from paddle.base import core
# --- Constants ---
KB = 1024
MB = 1024 * 1024
GB = 1024 * 1024 * 1024
# --- Formatting Helpers ---
def format_size(size_bytes):
if size_bytes == 0:
return "0 B"
if size_bytes < MB:
return f"{size_bytes / KB:.2f} KB"
if size_bytes < GB:
return f"{size_bytes / MB:.2f} MB"
return f"{size_bytes / GB:.2f} GB"
def print_table(title, headers, rows):
if not rows:
return
# Calculate widths
col_widths = [len(str(h)) for h in headers]
for row in rows:
for i, cell in enumerate(row):
if i < len(col_widths):
col_widths[i] = max(col_widths[i], len(str(cell)))
col_widths = [w + 2 for w in col_widths]
# Build lines
row_fmt = "|" + "|".join([f"{{:^{w}}}" for w in col_widths]) + "|"
header_sep = "+" + "+".join(["=" * w for w in col_widths]) + "+"
inner_sep = "+" + "+".join(["-" * w for w in col_widths]) + "+"
print(f"\n### {title}")
print(header_sep)
print(
"|" + "|".join([f"{h:^{w}}" for h, w in zip(headers, col_widths)]) + "|"
)
print(header_sep)
for i, row in enumerate(rows):
print(row_fmt.format(*[str(c) for c in row]))
if (
title == "Block Size Distribution"
and (i + 1) % 2 == 0
and i != len(rows) - 1
):
print(inner_sep)
elif title != "Block Size Distribution":
print(inner_sep)
if title == "Block Size Distribution":
print(header_sep)
class MemoryAnalysisTool:
def __init__(self):
raise TypeError("Utility class should not be instantiated.")
@classmethod
def vmm_max_free_size(
self, device_id: int | None = None
) -> tuple[int, int]:
name = 'paddle.device.cuda.vmm_max_free_size'
if not (core.is_compiled_with_cuda()):
raise ValueError(
f"The API {name} is not supported in CPU-only PaddlePaddle. Please reinstall PaddlePaddle with GPU support to call this API."
)
device_id = (
device_id
if device_id is not None
else core.get_cuda_current_device_id()
)
return core.vmm_max_free_size(device_id)
@classmethod
def vmm_free_block_info(
self,
device_id: int | None = None,
) -> list[list[tuple[int, int]]]:
name = 'paddle.device.cuda.vmm_free_block_info'
if not (core.is_compiled_with_cuda()):
raise ValueError(
f"The API {name} is not supported in CPU-only PaddlePaddle. Please reinstall PaddlePaddle with GPU support to call this API."
)
device_id = (
device_id
if device_id is not None
else core.get_cuda_current_device_id()
)
return core.vmm_free_block_info(device_id)
@classmethod
def all_block_info(
self,
device_id: int | None = None,
) -> list[list[tuple[int, int, bool]]]:
name = 'paddle.device.cuda.all_block_info'
if not (core.is_compiled_with_cuda()):
raise ValueError(
f"The API {name} is not supported in CPU-only PaddlePaddle. Please reinstall PaddlePaddle with GPU support to call this API."
)
device_id = (
device_id
if device_id is not None
else core.get_cuda_current_device_id()
)
info = core.all_block_info(device_id)
return [list(chunk) for chunk in info]
@classmethod
def vmm_all_block_info(
self,
device_id: int | None = None,
) -> list[list[tuple[int, int, bool]]]:
return self.all_block_info(device_id)
@classmethod
def vmm_large_all_block_info(
self,
device_id: int | None = None,
) -> list[list[tuple[int, int, bool]]]:
name = 'paddle.device.cuda.vmm_large_all_block_info'
if not (core.is_compiled_with_cuda()):
raise ValueError(
f"The API {name} is not supported in CPU-only PaddlePaddle. Please reinstall PaddlePaddle with GPU support to call this API."
)
device_id = (
device_id
if device_id is not None
else core.get_cuda_current_device_id()
)
return core.large_pool_block_info(device_id)
@classmethod
def vmm_small_all_block_info(
self,
device_id: int | None = None,
) -> list[list[tuple[int, int, bool]]]:
name = 'paddle.device.cuda.vmm_small_all_block_info'
if not (core.is_compiled_with_cuda()):
raise ValueError(
f"The API {name} is not supported in CPU-only PaddlePaddle. Please reinstall PaddlePaddle with GPU support to call this API."
)
device_id = (
device_id
if device_id is not None
else core.get_cuda_current_device_id()
)
return core.small_pool_block_info(device_id)
@classmethod
def memory_summary(self, device_id: int | None = None) -> None:
device_id = (
device_id
if device_id is not None
else core.get_cuda_current_device_id()
)
nvidia_smi_AVAILABLE = False
try:
# import nvidia_smi, pip install nvidia-ml-py3
import nvidia_smi
nvidia_smi_AVAILABLE = True
except ImportError:
nvidia_smi_AVAILABLE = False
THRESHOLDS = [
1 * MB,
10 * MB,
50 * MB,
100 * MB,
200 * MB,
400 * MB,
600 * MB,
800 * MB,
1 * GB,
2 * GB,
3 * GB,
]
RANGE_HEADERS = [
"[0B,1M)",
"[1M,10M)",
"[10M,50M)",
"[50M,100M)",
"[100M,200M)",
"[200M,400M)",
"[400M,600M)",
"[600M,800M)",
"[800M,1G)",
"[1G,2G)",
"[2G,3G)",
"[3G,+INF)",
]
allocator_lists = self.all_block_info(device_id=device_id)
# --- Feature 1: Global Summary with NVML & Rates ---
# 1.1 Get Paddle Stats
mem_allocated = paddle.device.cuda.memory_allocated()
max_mem_allocated = paddle.device.cuda.max_memory_allocated()
mem_reserved = paddle.device.cuda.memory_reserved()
max_mem_reserved = paddle.device.cuda.max_memory_reserved()
# 1.2 Calculate Rates (Utilization of the Reserved Pool)
# Rate = How much of the reserved pool is actually holding tensor data?
max_alloc_rate = (
((mem_reserved - max_mem_allocated) / mem_reserved)
if mem_reserved > 0
else 0.0
)
# 1.3 Get Physical Usage via nvidia_smi
phy_used_str = "N/A"
if nvidia_smi_AVAILABLE:
try:
nvidia_smi.nvmlInit()
handle = nvidia_smi.nvmlDeviceGetHandleByIndex(device_id)
info = nvidia_smi.nvmlDeviceGetMemoryInfo(handle)
phy_used_str = format_size(info.used)
phy_total_str = format_size(info.total)
# nvidia_smi.nvmlShutdown() # Optional, depends on lifecycle
except Exception as e:
phy_used_str = "Err"
phy_total_str = "Err"
else:
print(
"Place install nvidia-smi to check real memory usage, pip install command: `pip install nvidia-ml-py3`"
)
phy_used_str = "No nvidia_smi"
phy_total_str = "No nvidia_smi"
global_headers = [
"Allocators",
"Allocated",
"Max Alloc",
"Reserved",
"Max Reserved",
"Max Frag Rate",
"Phy GPU Used / Total",
]
global_rows = [
[
len(allocator_lists),
format_size(mem_allocated),
format_size(max_mem_allocated),
format_size(mem_reserved),
format_size(max_mem_reserved),
f"{max_alloc_rate:.2%}",
phy_used_str + ' / ' + phy_total_str,
]
]
print_table("Global Memory Snapshot", global_headers, global_rows)
# --- 2. Allocator Analysis ---
summary_rows = []
dist_rows = []
for idx, blocks in enumerate(allocator_lists):
allocator_name = f"Allocator_{idx}"
# A. Basic Counting
total_blocks = len(blocks)
free_blocks = 0
total_size = 0
free_size = 0
max_free_size = 0
max_used_size = 0
buckets = [[0, 0] for _ in range(len(RANGE_HEADERS))]
for size, addr, is_free in blocks:
total_size += size
if is_free:
free_blocks += 1
free_size += size
max_free_size = max(max_free_size, size)
else:
max_used_size = max(max_used_size, size)
# Bucket Mapping
b_idx = len(THRESHOLDS)
for i, t in enumerate(THRESHOLDS):
if size < t:
b_idx = i
break
buckets[b_idx][0 if is_free else 1] += 1
used_blocks = total_blocks - free_blocks
used_size = total_size - free_size
# B. Summary Row (Total -> Used -> Free)
summary_rows.append(
[
allocator_name,
total_blocks,
used_blocks,
free_blocks,
format_size(total_size),
format_size(used_size),
format_size(free_size),
format_size(max_used_size),
format_size(max_free_size),
]
)
# D. Distribution Rows
dist_rows.append(
[allocator_name, "Free Blocks"] + [b[0] for b in buckets]
)
dist_rows.append(
[allocator_name, "Used Blocks"] + [b[1] for b in buckets]
)
# --- 3. Render Outputs ---
sum_headers = [
"ID",
"Tot Blks",
"Used Blks",
"Free Blks",
"Tot Size",
"Used Size",
"Free Size",
"Max Used",
"Max Free",
]
print_table("Allocator Summary Statistics", sum_headers, summary_rows)
dist_headers = ["Allocator ID", "Block Type", *RANGE_HEADERS]
print_table("Block Size Distribution", dist_headers, dist_rows)
@classmethod
def allocate_record_table(self, data, output_filepath: str = ""):
if not data:
print("No data to display.")
return
print(f"Record data size: {len(data)}, start printing...")
headers = [
'Allocator_Instance',
'Is_Allocate',
'Seq_ID',
'Req_Size',
'Cur_Alloc',
'Cur_Rsrv',
]
formatted_row = []
all_lines = []
all_lines.append("\t".join(headers))
for row in data:
formatted_row = [
str(row[0]),
"Allocate" if row[1] else "Free",
str(row[2]),
str(row[3]),
str(row[4]),
str(row[5]),
]
line = "\t".join(formatted_row)
all_lines.append(line)
try:
with open(output_filepath, 'w', encoding='utf-8') as f:
f.write("\n".join(all_lines))
print(f"Data successfully written to: {output_filepath}")
except OSError as e:
print(f"Error writing to file {output_filepath}: {e}")
@classmethod
def allocate_record_plot(self, data, save_path: str = ""):
try:
import matplotlib.pyplot as plt
from matplotlib import ticker
except ImportError:
raise ImportError(
"matplotlib is required but not installed. Please install it using: pip install matplotlib"
)
if not data:
print("No data to plot.")
return
print(f"Record data size: {len(data)}, start plotting...")
data_np = np.array(data)
is_allocate = data_np[:, 1]
filter_mask = is_allocate == 1
data_np = data_np[filter_mask]
allocator_instance = data_np[:, 0] # allocator_instance not used
ids = data_np[:, 2]
sizes = data_np[:, 3]
allocated = data_np[:, 4]
reserved = data_np[:, 5]
LOG_START_VALUE = 1
plt.style.use('seaborn-v0_8-whitegrid')
fig, (ax1, ax2) = plt.subplots(
2,
1,
sharex=True,
figsize=(16, 10),
dpi=120,
gridspec_kw={'height_ratios': [3, 1], 'hspace': 0},
)
# allocated event plot
ax1.plot(
ids, sizes, color='#2ca02c', linestyle='-', linewidth=1, alpha=0.3
)
ax1.scatter(
ids,
sizes,
color='#2ca02c',
s=60,
alpha=1.0,
edgecolors='white',
linewidth=0.5,
label='Request Size',
zorder=5,
)
ax1.set_ylabel(
'Request Size (Linear Scale)',
fontsize=12,
fontweight='bold',
labelpad=10,
)
ax1.set_title(
'Paddle GPU Memory Allocation Analysis',
fontsize=16,
fontweight='bold',
pad=20,
)
ax1.set_ylim(bottom=LOG_START_VALUE)
ax1.tick_params(axis='x', length=0)
plt.setp(ax1.get_xticklabels(), visible=False)
# memory allocated, reserved plot
ax2.plot(
ids,
reserved,
color='#d62728',
linestyle='--',
linewidth=1.5,
alpha=0.8,
label='Reserved (Pool)',
)
ax2.fill_between(ids, 0, reserved, color='#d62728', alpha=0.1)
ax2.plot(
ids,
allocated,
color='#1f77b4',
linestyle='-',
linewidth=2,
alpha=0.9,
label='Allocated (Used)',
)
ax2.fill_between(ids, 0, allocated, color='#1f77b4', alpha=0.15)
ax2.invert_yaxis()
ax2.set_ylim(reserved.max() * 3.0, LOG_START_VALUE)
# ax2.set_yscale('symlog', linthresh=1024 * 1024)
ax2.set_ylabel(
'Pool Status (Inverted)',
fontsize=11,
fontweight='bold',
labelpad=10,
)
ax2.set_xlabel('')
ax2.tick_params(axis='x', which='both', length=0)
plt.setp(ax2.get_xticklabels(), visible=False)
# y axis setting 0
def y_axis_formatter(x, pos):
val = abs(x)
if val <= LOG_START_VALUE * 1.5:
return '0'
return format_size(val).replace(" ", "")
formatter = ticker.FuncFormatter(y_axis_formatter)
ax1.yaxis.set_major_formatter(formatter)
ax2.yaxis.set_major_formatter(formatter)
for ax in [ax1, ax2]:
current_ticks = ax.get_yticks().tolist()
if LOG_START_VALUE not in current_ticks:
current_ticks.append(LOG_START_VALUE)
ax.set_yticks(sorted(current_ticks))
# axis setting
for ax in [ax1, ax2]:
for spine in ax.spines.values():
spine.set_edgecolor('black')
spine.set_linewidth(1.5)
ax.tick_params(
axis='both', which='major', colors='black', width=1.0, length=5
)
lines1, labels1 = ax1.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax1.legend(
lines1 + lines2,
labels1 + labels2,
loc='upper right',
fontsize=10,
frameon=True,
facecolor='white',
framealpha=0.9,
edgecolor='black',
shadow=False,
)
plt.tight_layout()
plt.subplots_adjust(hspace=0.05)
plt.savefig(save_path)
plt.close()
print(f"Analysis plot saved to: {save_path}")