Files
paddlepaddle--paddle/test/test_flashmask_ci/test_fwd_md5sum.py
T
2026-07-13 12:40:42 +08:00

306 lines
8.5 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.
import itertools
import json
import os
from functools import partial
import numpy as np
import pytest
from generate_startend_row_indices import (
generate_causal_blockwise_mask,
generate_causal_document_mask,
generate_document_mask,
generate_global_sliding_window_mask,
generate_prefix_lm_causal_mask,
generate_prefix_lm_document_mask,
generate_qk_sparse_mask,
generate_random_eviction_mask,
generate_share_question_mask,
generate_sliding_window_mask,
)
import paddle
from paddle.nn.functional.flash_attention import flashmask_attention
GEN_FUNCTIONS = [
# partial(generate_none_mask, causal=False),
# partial(generate_none_mask, causal=True),
partial(generate_sliding_window_mask),
partial(generate_causal_document_mask),
partial(generate_document_mask),
partial(generate_share_question_mask),
partial(generate_global_sliding_window_mask),
partial(generate_causal_blockwise_mask),
partial(generate_prefix_lm_document_mask),
partial(generate_prefix_lm_causal_mask),
partial(generate_qk_sparse_mask),
partial(generate_random_eviction_mask),
]
def record_gt(output_file="flashmask_gt.json"):
gt_records = {}
param_combinations = generate_all_param_combinations()
print(
f"Start recording test cases, {len(param_combinations)} test cases in total."
)
for i, params in enumerate(param_combinations):
try:
out = run_flashmask_forward(**params)
md5sum = out._md5sum()
param_key = generate_param_key(params)
gt_records[param_key] = md5sum
if (i + 1) % 10 == 0:
print(f"{i + 1}/{len(param_combinations)} test cases recorded")
except Exception as e:
print(f"Skipping test case due to exception: {params}: {e}")
continue
gt_records["gt_commit_id"] = input(
"Please input the commit ID of fwd GT md5sum: "
)
gt_records["gt_commit_msg"] = input(
"Please input the commit msg of fwd GT md5sum: "
)
with open(output_file, 'w') as f:
json.dump(gt_records, f, indent=2)
print(
f"Ground truth saved to '{output_file}', {len(gt_records)} test cases recorded."
)
return gt_records
def run_flashmask_forward(
batch_size,
seqlen_q,
seqlen_k,
nheads,
nheads_kv,
d,
dv,
nheads_startend_row_indices,
fa_version,
dtype,
gen_startend_row_indices,
softcap=0.0,
):
paddle.seed(2024)
np.random.seed(2024)
assert nheads % nheads_kv == 0
q = paddle.randn(shape=[batch_size, seqlen_q, nheads, d], dtype=dtype)
k = paddle.randn(shape=[batch_size, seqlen_k, nheads_kv, d], dtype=dtype)
v = paddle.randn(shape=[batch_size, seqlen_k, nheads_kv, dv], dtype=dtype)
startend_row_indices, causal = gen_startend_row_indices(
batch_size, seqlen_q, seqlen_k, nheads_startend_row_indices
)
if startend_row_indices is None and causal and d == 80:
pytest.skip(
"Skipping because running headdim 80 with flash_attn in causal mask"
)
if fa_version == 2:
paddle.set_flags({'FLAGS_flash_attn_version': 2})
elif fa_version == 3:
paddle.set_flags({'FLAGS_flash_attn_version': 3})
else:
raise ValueError(f"Invalid flash attention version: {fa_version}")
out, lse = flashmask_attention(
q,
k,
v,
startend_row_indices=startend_row_indices,
causal=causal,
return_softmax_lse=True,
)
return out
# Shape Combination
shape_cases = [
(1, 8192, 32768 + 1024, 2, 1),
(2840, 32, 32, 16, 4),
(1, 300, 300, 16, 16),
(1, 128, 127, 1, 1),
(2, 16384, 16383, 4, 1),
]
def generate_shapes():
for batch_size, seqlen_q, seqlen_k, nheads, nheads_kv in shape_cases:
nheads_startend_row_indices_values = [1, nheads_kv]
for nheads_startend_row_indices in nheads_startend_row_indices_values:
yield (
batch_size,
seqlen_q,
seqlen_k,
nheads,
nheads_kv,
nheads_startend_row_indices,
)
def generate_all_param_combinations():
dtypes = [paddle.bfloat16]
fa_versions = [3]
d_dv_combinations = [(128, 128), (80, 80), (64, 64)]
shapes = list(generate_shapes())
gen_funcs = GEN_FUNCTIONS
combinations = []
for (
batch_size,
seqlen_q,
seqlen_k,
nheads,
nheads_kv,
nheads_startend_row_indices,
), dtype, fa_version, (d, dv), gen_func in itertools.product(
shapes, dtypes, fa_versions, d_dv_combinations, gen_funcs
):
params = {
'batch_size': batch_size,
'seqlen_q': seqlen_q,
'seqlen_k': seqlen_k,
'nheads': nheads,
'nheads_kv': nheads_kv,
'd': d,
'dv': dv,
'nheads_startend_row_indices': nheads_startend_row_indices,
'fa_version': fa_version,
'dtype': dtype,
'gen_startend_row_indices': gen_func,
'softcap': 0.0,
}
combinations.append(params)
return combinations
def generate_param_key(params):
gen_func_index = get_gen_func_index(params['gen_startend_row_indices'])
nheads_startend = params['nheads_startend_row_indices']
dtype_index = get_dtype_index(params['dtype'])
if isinstance(nheads_startend, (list, tuple)):
nheads_startend_str = '_'.join(map(str, nheads_startend))
else:
nheads_startend_str = str(nheads_startend)
return (
f"gen_startend_row_indices{gen_func_index}-"
f"{params['batch_size']}-{params['seqlen_q']}-{params['seqlen_k']}-"
f"{params['nheads']}-{params['nheads_kv']}-{nheads_startend_str}-"
f"{params['d']}-{params['dv']}-{params['fa_version']}-dtype{dtype_index}"
)
def get_gen_func_index(gen_func):
for i, func in enumerate(GEN_FUNCTIONS):
if gen_func == func or (
hasattr(gen_func, 'func') and gen_func.func == func.func
):
return i
return -1
def get_dtype_index(dtype):
dtype_list = [paddle.bfloat16]
for i, dt in enumerate(dtype_list):
if dtype == dt:
return i
return -1
gt_records = {}
try:
with open("flashmask_gt.json", 'r') as f:
gt_records = json.load(f)
except FileNotFoundError:
pass
@pytest.mark.parametrize("dtype", [paddle.bfloat16])
@pytest.mark.parametrize("fa_version", [3])
@pytest.mark.parametrize("d, dv", [(128, 128), (80, 80), (64, 64)])
@pytest.mark.parametrize(
"batch_size, seqlen_q, seqlen_k, nheads, nheads_kv, nheads_startend_row_indices",
list(generate_shapes()),
)
@pytest.mark.parametrize(
"gen_startend_row_indices",
GEN_FUNCTIONS,
)
def test_flashmask_md5(
batch_size,
seqlen_q,
seqlen_k,
nheads,
nheads_kv,
d,
dv,
nheads_startend_row_indices,
fa_version,
dtype,
gen_startend_row_indices,
softcap=0.0,
):
params = {
'batch_size': batch_size,
'seqlen_q': seqlen_q,
'seqlen_k': seqlen_k,
'nheads': nheads,
'nheads_kv': nheads_kv,
'd': d,
'dv': dv,
'nheads_startend_row_indices': nheads_startend_row_indices,
'fa_version': fa_version,
'dtype': dtype,
'gen_startend_row_indices': gen_startend_row_indices,
'softcap': softcap,
}
param_key = generate_param_key(params)
if param_key not in gt_records:
pytest.skip(f"No ground truth record for {param_key}")
out = run_flashmask_forward(**params)
actual_md5 = out._md5sum()
expected_md5 = gt_records[param_key]
assert actual_md5 == expected_md5, (
f"MD5 mismatch for {param_key}\nExpected: {expected_md5}\nGot: {actual_md5}"
)
if __name__ == "__main__":
if not os.path.exists("flashmask_gt.json"):
print("Start recording ground truth...")
record_gt()
else:
print("Ground truth file exists, run pytest to execute tests")