258 lines
11 KiB
Python
258 lines
11 KiB
Python
import torch
|
|
import pytest
|
|
import sys
|
|
import os
|
|
import numpy as np
|
|
from tqdm import tqdm
|
|
|
|
from .utils import generate_block_sparse_mask_for_function, create_full_mask_from_block_mask
|
|
# Use installed package
|
|
# from fastvideo_kernel import video_sparse_attn as block_sparse_attn
|
|
|
|
BLOCK_M = 64
|
|
BLOCK_N = 64
|
|
|
|
def pytorch_test(Q, K, V, block_sparse_mask, dO):
|
|
q_ = Q.clone().float().requires_grad_()
|
|
k_ = K.clone().float().requires_grad_()
|
|
v_ = V.clone().float().requires_grad_()
|
|
|
|
QK = torch.matmul(q_, k_.transpose(-2, -1))
|
|
QK /= (q_.size(-1) ** 0.5)
|
|
QK = QK.masked_fill(~block_sparse_mask.unsqueeze(0), float('-inf'))
|
|
|
|
QK = torch.nn.functional.softmax(QK, dim=-1)
|
|
output = torch.matmul(QK, v_)
|
|
|
|
dO_ = dO
|
|
output.backward(dO_)
|
|
return (
|
|
output.to(torch.bfloat16),
|
|
q_.grad.to(torch.bfloat16),
|
|
k_.grad.to(torch.bfloat16),
|
|
v_.grad.to(torch.bfloat16),
|
|
)
|
|
|
|
|
|
def block_sparse_kernel_test(Q, K, V, block_sparse_mask, variable_block_sizes, q_non_pad_index, kv_non_pad_index, q_num_blocks, kv_num_blocks, dO):
|
|
Q = Q.detach().requires_grad_()
|
|
K = K.detach().requires_grad_()
|
|
V = V.detach().requires_grad_()
|
|
|
|
q_padded = vsa_pad(Q, q_non_pad_index, q_num_blocks, BLOCK_M)
|
|
k_padded = vsa_pad(K, kv_non_pad_index, kv_num_blocks, BLOCK_M)
|
|
v_padded = vsa_pad(V, kv_non_pad_index, kv_num_blocks, BLOCK_M)
|
|
# Use autograd-enabled wrapper (internally dispatches to SM90 kernel or Triton)
|
|
from fastvideo_kernel.block_sparse_attn import block_sparse_attn
|
|
output_padded, _aux = block_sparse_attn(
|
|
q_padded, k_padded, v_padded, block_sparse_mask, variable_block_sizes
|
|
)
|
|
|
|
output = output_padded[:, :, q_non_pad_index, :]
|
|
output.backward(dO)
|
|
return output, Q.grad, K.grad, V.grad
|
|
|
|
|
|
def get_non_pad_index(
|
|
vid_len: torch.LongTensor,
|
|
n_win: int,
|
|
win_size: int,
|
|
):
|
|
device = vid_len.device
|
|
starts_pad = torch.arange(n_win, device=device) * win_size
|
|
index_pad = starts_pad[:, None] + torch.arange(win_size, device=device)[None, :]
|
|
index_mask = torch.arange(win_size, device=device)[None, :] < vid_len[:, None]
|
|
|
|
return index_pad[index_mask]
|
|
|
|
def generate_tensor(shape, dtype, device):
|
|
tensor = torch.randn(shape, dtype=dtype, device=device)
|
|
return tensor
|
|
|
|
def generate_variable_block_sizes(num_blocks, min_size=16, max_size=64, device="cuda"):
|
|
return torch.randint(min_size, max_size + 1, (num_blocks,), device=device, dtype=torch.int32)
|
|
|
|
|
|
def vsa_pad(x, non_pad_index, num_blocks, block_size):
|
|
padded_x = torch.zeros((1, x.shape[1], num_blocks * BLOCK_M, x.shape[3]), device=x.device, dtype=x.dtype)
|
|
padded_x[:, :, non_pad_index, :] = x
|
|
return padded_x
|
|
|
|
def check_correctness(h, d, num_blocks, k, num_iterations=20, error_mode='all'):
|
|
results = {
|
|
'gO': {'sum_diff': 0.0, 'sum_abs': 0.0, 'max_diff': 0.0},
|
|
'gQ': {'sum_diff': 0.0, 'sum_abs': 0.0, 'max_diff': 0.0},
|
|
'gK': {'sum_diff': 0.0, 'sum_abs': 0.0, 'max_diff': 0.0},
|
|
'gV': {'sum_diff': 0.0, 'sum_abs': 0.0, 'max_diff': 0.0},
|
|
}
|
|
|
|
device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
variable_block_sizes = generate_variable_block_sizes(num_blocks, device=device)
|
|
S = int(variable_block_sizes.sum().item())
|
|
padded_S = num_blocks * BLOCK_M
|
|
non_pad_index = get_non_pad_index(variable_block_sizes, num_blocks, BLOCK_M)
|
|
block_mask = generate_block_sparse_mask_for_function(h, num_blocks, num_blocks, k, device)
|
|
full_mask = create_full_mask_from_block_mask(block_mask, variable_block_sizes, variable_block_sizes, device)
|
|
for _ in range(num_iterations):
|
|
Q = generate_tensor((1, h, S, d), torch.bfloat16, device)
|
|
K = generate_tensor((1, h, S, d), torch.bfloat16, device)
|
|
V = generate_tensor((1, h, S, d), torch.bfloat16, device)
|
|
dO = generate_tensor((1, h, S, d), torch.bfloat16, device)
|
|
|
|
# print(Q.shape, K.shape, V.shape, dO.shape)
|
|
|
|
# dO_padded = torch.zeros_like(dO_padded)
|
|
# dO_padded[:, :, non_pad_index, :] = dO
|
|
|
|
pt_o, pt_qg, pt_kg, pt_vg = pytorch_test(Q, K, V, full_mask, dO)
|
|
bs_o, bs_qg, bs_kg, bs_vg = block_sparse_kernel_test(Q, K, V, block_mask.unsqueeze(0), variable_block_sizes, non_pad_index, non_pad_index, num_blocks, num_blocks, dO)
|
|
for name, (pt, bs) in zip(['gQ', 'gK', 'gV', 'gO'], [(pt_qg, bs_qg), (pt_kg, bs_kg), (pt_vg, bs_vg), (pt_o, bs_o)]):
|
|
if bs is not None:
|
|
diff = pt - bs
|
|
abs_diff = torch.abs(diff)
|
|
results[name]['sum_diff'] += torch.sum(abs_diff).item()
|
|
results[name]['sum_abs'] += torch.sum(torch.abs(pt)).item()
|
|
rel_max_diff = torch.max(abs_diff) / torch.mean(torch.abs(pt))
|
|
results[name]['max_diff'] = max(results[name]['max_diff'], rel_max_diff.item())
|
|
if torch.cuda.is_available():
|
|
torch.cuda.empty_cache()
|
|
|
|
total_elements = h * S * d * num_iterations
|
|
for name, data in results.items():
|
|
avg_diff = data['sum_diff'] / total_elements
|
|
max_diff = data['max_diff']
|
|
results[name] = {'avg_diff': avg_diff, 'max_diff': max_diff}
|
|
|
|
return results
|
|
|
|
def check_correctness_qkdiff(h, d, num_q_blocks, num_kv_blocks, k, num_iterations=20, error_mode='all'):
|
|
results = {
|
|
'gO': {'sum_diff': 0.0, 'sum_abs': 0.0, 'max_diff': 0.0},
|
|
'gQ': {'sum_diff': 0.0, 'sum_abs': 0.0, 'max_diff': 0.0},
|
|
'gK': {'sum_diff': 0.0, 'sum_abs': 0.0, 'max_diff': 0.0},
|
|
'gV': {'sum_diff': 0.0, 'sum_abs': 0.0, 'max_diff': 0.0},
|
|
}
|
|
|
|
device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
q_variable_block_sizes = generate_variable_block_sizes(num_q_blocks, device=device)
|
|
kv_variable_block_sizes = generate_variable_block_sizes(num_kv_blocks, device=device)
|
|
|
|
S_q = int(q_variable_block_sizes.sum().item())
|
|
S_kv = int(kv_variable_block_sizes.sum().item())
|
|
|
|
q_non_pad_index = get_non_pad_index(q_variable_block_sizes, num_q_blocks, BLOCK_M)
|
|
kv_non_pad_index = get_non_pad_index(kv_variable_block_sizes, num_kv_blocks, BLOCK_M)
|
|
|
|
block_mask = generate_block_sparse_mask_for_function(h, num_q_blocks, num_kv_blocks, k, device)
|
|
full_mask = create_full_mask_from_block_mask(block_mask, q_variable_block_sizes, kv_variable_block_sizes, device)
|
|
|
|
for _ in range(num_iterations):
|
|
Q = generate_tensor((1, h, S_q, d), torch.bfloat16, device)
|
|
K = generate_tensor((1, h, S_kv, d), torch.bfloat16, device)
|
|
V = generate_tensor((1, h, S_kv, d), torch.bfloat16, device)
|
|
dO = generate_tensor((1, h, S_q, d), torch.bfloat16, device)
|
|
|
|
# print(Q.shape, K.shape, V.shape, dO.shape)
|
|
|
|
pt_o, pt_qg, pt_kg, pt_vg = pytorch_test(Q, K, V, full_mask, dO)
|
|
bs_o, bs_qg, bs_kg, bs_vg = block_sparse_kernel_test(Q, K, V, block_mask.unsqueeze(0), kv_variable_block_sizes, q_non_pad_index, kv_non_pad_index, num_q_blocks, num_kv_blocks, dO)
|
|
|
|
for name, (pt, bs) in zip(['gQ', 'gK', 'gV', 'gO'], [(pt_qg, bs_qg), (pt_kg, bs_kg), (pt_vg, bs_vg), (pt_o, bs_o)]):
|
|
if bs is not None:
|
|
diff = pt - bs
|
|
abs_diff = torch.abs(diff)
|
|
results[name]['sum_diff'] += torch.sum(abs_diff).item()
|
|
results[name]['sum_abs'] += torch.sum(torch.abs(pt)).item()
|
|
rel_max_diff = torch.max(abs_diff) / torch.mean(torch.abs(pt))
|
|
results[name]['max_diff'] = max(results[name]['max_diff'], rel_max_diff.item())
|
|
if torch.cuda.is_available():
|
|
torch.cuda.empty_cache()
|
|
|
|
total_elements_q = h * S_q * d * num_iterations
|
|
total_elements_kv = h * S_kv * d * num_iterations
|
|
|
|
for name, data in results.items():
|
|
total_elements = total_elements_q if name in ['gQ', 'gO'] else total_elements_kv
|
|
avg_diff = data['sum_diff'] / total_elements
|
|
max_diff = data['max_diff']
|
|
results[name] = {'avg_diff': avg_diff, 'max_diff': max_diff}
|
|
|
|
return results
|
|
|
|
def generate_error_graphs(h, d, error_mode='all'):
|
|
test_configs = [
|
|
{"num_blocks": 16, "k": 2, "description": "Small sequence"},
|
|
{"num_blocks": 32, "k": 4, "description": "Medium sequence"},
|
|
{"num_blocks": 53, "k": 6, "description": "Large sequence"},
|
|
]
|
|
|
|
print(f"\nError Analysis for h={h}, d={d}, mode={error_mode}")
|
|
print("=" * 150)
|
|
print(f"{'Config':<20} {'Blocks':<8} {'K':<4} "
|
|
f"{'gQ Avg':<12} {'Rel gQ Max':<12} "
|
|
f"{'gK Avg':<12} {'Rel gK Max':<12} "
|
|
f"{'gV Avg':<12} {'Rel gV Max':<12} "
|
|
f"{'gO Avg':<12} {'Rel gO Max':<12}")
|
|
print("-" * 150)
|
|
|
|
for config in test_configs:
|
|
num_blocks = config["num_blocks"]
|
|
k = config["k"]
|
|
description = config["description"]
|
|
results = check_correctness(h, d, num_blocks, k, error_mode=error_mode)
|
|
print(f"{description:<20} {num_blocks:<8} {k:<4} "
|
|
f"{results['gQ']['avg_diff']:<12.6e} {results['gQ']['max_diff']:<12.6e} "
|
|
f"{results['gK']['avg_diff']:<12.6e} {results['gK']['max_diff']:<12.6e} "
|
|
f"{results['gV']['avg_diff']:<12.6e} {results['gV']['max_diff']:<12.6e} "
|
|
f"{results['gO']['avg_diff']:<12.6e} {results['gO']['max_diff']:<12.6e}")
|
|
|
|
print("-" * 150)
|
|
|
|
def generate_error_graphs_qkdiff(h, d, error_mode='all'):
|
|
test_configs = [
|
|
{"num_q_blocks": 16, "num_kv_blocks": 32, "k": 2, "description": "Small Q, Med KV"},
|
|
{"num_q_blocks": 32, "num_kv_blocks": 16, "k": 4, "description": "Med Q, Small KV"},
|
|
{"num_q_blocks": 53, "num_kv_blocks": 32, "k": 6, "description": "Large Q, Med KV"},
|
|
{"num_q_blocks": 16, "num_kv_blocks": 48, "k": 2, "description": "Small Q, Large KV"},
|
|
{"num_q_blocks": 48, "num_kv_blocks": 16, "k": 2, "description": "Large Q, Small KV"},
|
|
]
|
|
|
|
print(f"\nError Analysis (QK Diff) for h={h}, d={d}, mode={error_mode}")
|
|
print("=" * 150)
|
|
print(f"{'Config':<20} {'Q Blks':<8} {'KV Blks':<8} {'K':<4} "
|
|
f"{'gQ Avg':<12} {'Rel gQ Max':<12} "
|
|
f"{'gK Avg':<12} {'Rel gK Max':<12} "
|
|
f"{'gV Avg':<12} {'Rel gV Max':<12} "
|
|
f"{'gO Avg':<12} {'Rel gO Max':<12}")
|
|
print("-" * 150)
|
|
|
|
for config in test_configs:
|
|
num_q_blocks = config["num_q_blocks"]
|
|
num_kv_blocks = config["num_kv_blocks"]
|
|
k = config["k"]
|
|
description = config["description"]
|
|
results = check_correctness_qkdiff(h, d, num_q_blocks, num_kv_blocks, k, error_mode=error_mode)
|
|
print(f"{description:<20} {num_q_blocks:<8} {num_kv_blocks:<8} {k:<4} "
|
|
f"{results['gQ']['avg_diff']:<12.6e} {results['gQ']['max_diff']:<12.6e} "
|
|
f"{results['gK']['avg_diff']:<12.6e} {results['gK']['max_diff']:<12.6e} "
|
|
f"{results['gV']['avg_diff']:<12.6e} {results['gV']['max_diff']:<12.6e} "
|
|
f"{results['gO']['avg_diff']:<12.6e} {results['gO']['max_diff']:<12.6e}")
|
|
|
|
print("-" * 150)
|
|
|
|
def test_video_sparse_attention_backward():
|
|
if not torch.cuda.is_available():
|
|
return
|
|
|
|
h, d = 16, 128
|
|
print("Block Sparse Attention with Variable Block Sizes Analysis")
|
|
print("=" * 60)
|
|
for mode in ['backward']:
|
|
generate_error_graphs(h, d, error_mode=mode)
|
|
generate_error_graphs_qkdiff(h, d, error_mode=mode)
|
|
print("\nAnalysis completed for all modes.")
|
|
|
|
if __name__ == "__main__":
|
|
test_video_sparse_attention_backward()
|
|
|