已合并
flexattention: port flexattention template to 2.13 #44356
stonexxx创建于 8月11日
flexattention: port flexattention template to 2.13 #44356
已合并
共 12 个文件变更+7995-1543
| @@ -112,6 +112,7 @@ def _load_triton_backend(): | |||
| 112 | _register_npu_inductor_grouped_mm, | 112 | _register_npu_inductor_grouped_mm, |
| 113 | _register_npu_inductor_mm, | 113 | _register_npu_inductor_mm, |
| 114 | _validate_device, | 114 | _validate_device, |
| 115 | + patch_flex_attention, | ||
| 115 | ) | 116 | ) |
| 116 | from .lowering import make_reduction | 117 | from .lowering import make_reduction |
| 117 | from .runtime import ( | 118 | from .runtime import ( |
| @@ -125,6 +126,28 @@ def _load_triton_backend(): | |||
| 125 | from .graph import patch_count_bytes | 126 | from .graph import patch_count_bytes |
| 126 | from .autotune_process import patch_tuning_process | 127 | from .autotune_process import patch_tuning_process |
| 127 | flex_attention._validate_device = _validate_device | 128 | flex_attention._validate_device = _validate_device |
| 129 | + patch_flex_attention() | ||
| 130 | + | ||
| 131 | + def _patch_flex_attention_singleton_sort(): | ||
| 132 | + original = getattr(flex_attention, "_dense_to_ordered", None) | ||
| 133 | + if original is None or getattr(original, "_torch_npu_singleton_sort_patch", False): | ||
| 134 | + return | ||
| 135 | + | ||
| 136 | + def _dense_to_ordered_npu_safe(dense_mask): | ||
| 137 | + if dense_mask.ndim > 0 and dense_mask.size(-1) == 1: | ||
| 138 | + dense_mask = dense_mask.to(dtype=torch.int32) | ||
| 139 | + num_blocks_in_row = dense_mask.sum(dim=-1) | ||
| 140 | + col_indices = torch.zeros_like(dense_mask, dtype=torch.int32) | ||
| 141 | + return ( | ||
| 142 | + num_blocks_in_row.to(torch.int32, memory_format=torch.contiguous_format), | ||
| 143 | + col_indices.to(torch.int32, memory_format=torch.contiguous_format), | ||
| 144 | + ) | ||
| 145 | + return original(dense_mask) | ||
| 146 | + | ||
| 147 | + _dense_to_ordered_npu_safe._torch_npu_singleton_sort_patch = True | ||
| 148 | + flex_attention._dense_to_ordered = _dense_to_ordered_npu_safe | ||
| 149 | + | ||
| 150 | + _patch_flex_attention_singleton_sort() | ||
| 128 | 151 | ||
| 129 | def _inductor_register_backend_for_device(): | 152 | def _inductor_register_backend_for_device(): |
| 130 | from .codegen.cpp_wrapper_npu import CppWrapperNpu | 153 | from .codegen.cpp_wrapper_npu import CppWrapperNpu |
| @@ -38,6 +38,7 @@ from ..lowering_fx import ( | |||
| 38 | ) | 38 | ) |
| 39 | from ..fx_passes.utils.schedule_node_utils import is_multi_stream | 39 | from ..fx_passes.utils.schedule_node_utils import is_multi_stream |
| 40 | from ..config import log | 40 | from ..config import log |
| 41 | +from ..select_algorithm import NPUFlexAttentionDkdvTemplateBuffer | ||
| 41 | 42 | ||
| 42 | 43 | ||
| 43 | def flatten_groups(nums): | 44 | def flatten_groups(nums): |
| @@ -50,6 +51,7 @@ def flatten_groups(nums): | |||
| 50 | res.append(i) | 51 | res.append(i) |
| 51 | return res | 52 | return res |
| 52 | 53 | ||
| 54 | + | ||
| 53 | class NPUNoLinearTritonScheduling(TritonScheduling): | 55 | class NPUNoLinearTritonScheduling(TritonScheduling): |
| 54 | kernel_type = NPUTritonKernel | 56 | kernel_type = NPUTritonKernel |
| 55 | 57 | ||
| @@ -236,6 +238,14 @@ class NPUTritonScheduling(TritonScheduling): | |||
| 236 | self.scheduler.free_buffers() | 238 | self.scheduler.free_buffers() |
| 237 | 239 | ||
| 238 | def codegen_template(self, template_node, epilogue_nodes, only_gen_src_code=False): | 240 | def codegen_template(self, template_node, epilogue_nodes, only_gen_src_code=False): |
| 241 | + if isinstance(template_node.node, NPUFlexAttentionDkdvTemplateBuffer): | ||
| 242 | + if only_gen_src_code: | ||
| 243 | + raise NotImplementedError( | ||
| 244 | + "source-only codegen is unsupported for composite dK/dV templates" | ||
| 245 | + ) | ||
| 246 | + return self.codegen_flex_attention_dkdv_template( | ||
| 247 | + template_node, epilogue_nodes | ||
| 248 | + ) | ||
| 239 | _, (numel, rnumel) = template_node.group | 249 | _, (numel, rnumel) = template_node.group |
| 240 | assert rnumel == 1 | 250 | assert rnumel == 1 |
| 241 | kernel, render = template_node.node.make_kernel_render(template_node.node) | 251 | kernel, render = template_node.node.make_kernel_render(template_node.node) |
| @@ -244,21 +254,41 @@ class NPUTritonScheduling(TritonScheduling): | |||
| 244 | for node in [template_node, *epilogue_nodes]: | 254 | for node in [template_node, *epilogue_nodes]: |
| 245 | node.mark_run() | 255 | node.mark_run() |
| 246 | partial_code = render() | 256 | partial_code = render() |
| 247 | - with kernel.set_subgraph_body("<STORE_OUTPUT>"): | 257 | + # Handle both legacy "<STORE_OUTPUT>" (without index) and torch |
| 248 | - for node in epilogue_nodes: | 258 | + # 2.10's "<STORE_OUTPUT_N>" (with index) subgraph body naming. |
| 249 | - node.codegen(kernel.split_and_set_ranges(node.get_ranges())) | 259 | + if "<STORE_OUTPUT>" in kernel.subgraph_bodies: |
| 260 | + with kernel.set_subgraph_body("<STORE_OUTPUT>"): | ||
| 261 | + for node in epilogue_nodes: | ||
| 262 | + node.codegen(kernel.split_and_set_ranges(node.get_ranges())) | ||
| 263 | + # torch 2.10+ creates indexed store_output subgraphs. | ||
| 264 | + if hasattr(kernel, "get_store_output_count"): | ||
| 265 | + num_store_subgraphs = kernel.get_store_output_count() | ||
| 266 | + for i in range(num_store_subgraphs): | ||
| 267 | + subgraph_name = kernel._get_store_output_subgraph_name(i) | ||
| 268 | + if subgraph_name in kernel.subgraph_bodies: | ||
| 269 | + with kernel.set_subgraph_body(subgraph_name): | ||
| 270 | + for node in epilogue_nodes: | ||
| 271 | + node.codegen(kernel.split_and_set_ranges(node.get_ranges())) | ||
| 250 | 272 | ||
| 251 | if not isinstance(partial_code, str): | 273 | if not isinstance(partial_code, str): |
| 252 | partial_code.finalize_hook("<DEF_KERNEL>") | 274 | partial_code.finalize_hook("<DEF_KERNEL>") |
| 253 | partial_code.finalize_hook("<ARGDEFS>", strict=False) | 275 | partial_code.finalize_hook("<ARGDEFS>", strict=False) |
| 254 | # finalize must be called after adding epilogue above | 276 | # finalize must be called after adding epilogue above |
| 255 | with V.set_kernel_handler(kernel): | 277 | with V.set_kernel_handler(kernel): |
| 256 | - with kernel.set_subgraph_body("<STORE_OUTPUT>"): | 278 | + # Finalize legacy "<STORE_OUTPUT>" (without index) if present. |
| 257 | - if isinstance(partial_code, str): | 279 | + has_store_output = "<STORE_OUTPUT>" in kernel.subgraph_bodies |
| 258 | - src_code = partial_code | 280 | + if has_store_output and not isinstance(partial_code, str): |
| 259 | - else: | 281 | + with kernel.set_subgraph_body("<STORE_OUTPUT>"): |
| 260 | partial_code.finalize_hook("<STORE_OUTPUT>") | 282 | partial_code.finalize_hook("<STORE_OUTPUT>") |
| 261 | - src_code = partial_code.code | 283 | + # Now safe to extract the final source code. |
| 284 | + # Use finalize_remaining() to catch any leftover hooks (e.g. | ||
| 285 | + # torch 2.10's indexed "<STORE_OUTPUT_N>" hooks). | ||
| 286 | + if isinstance(partial_code, str): | ||
| 287 | + src_code = partial_code | ||
| 288 | + elif hasattr(partial_code, "finalize_remaining"): | ||
| 289 | + src_code = partial_code.finalize_remaining() | ||
| 290 | + else: | ||
| 291 | + src_code = partial_code.code | ||
| 262 | node_schedule = [template_node, *epilogue_nodes] | 292 | node_schedule = [template_node, *epilogue_nodes] |
| 263 | 293 | ||
| 264 | if config.benchmark_kernel: | 294 | if config.benchmark_kernel: |
| @@ -286,6 +316,113 @@ class NPUTritonScheduling(TritonScheduling): | |||
| 286 | self.scheduler.free_buffers() | 316 | self.scheduler.free_buffers() |
| 287 | return None | 317 | return None |
| 288 | 318 | ||
| 319 | + def codegen_flex_attention_dkdv_template( | ||
| 320 | + self, template_node, epilogue_nodes | ||
| 321 | + ): | ||
| 322 | + if epilogue_nodes: | ||
| 323 | + raise NotImplementedError( | ||
| 324 | + "epilogue fusion is unsupported for composite dK/dV templates" | ||
| 325 | + ) | ||
| 326 | + composite = template_node.node | ||
| 327 | + | ||
| 328 | + def render_source(kernel, render): | ||
| 329 | + with kernel: | ||
| 330 | + partial_code = render() | ||
| 331 | + if not isinstance(partial_code, str): | ||
| 332 | + partial_code.finalize_hook("<DEF_KERNEL>") | ||
| 333 | + partial_code.finalize_hook("<ARGDEFS>", strict=False) | ||
| 334 | + with V.set_kernel_handler(kernel): | ||
| 335 | + if not isinstance(partial_code, str): | ||
| 336 | + if "<STORE_OUTPUT>" in kernel.subgraph_bodies: | ||
| 337 | + partial_code.finalize_hook("<STORE_OUTPUT>") | ||
| 338 | + return partial_code.code | ||
| 339 | + return partial_code | ||
| 340 | + | ||
| 341 | + legacy_kernel, legacy_render = composite.make_kernel_render(composite) | ||
| 342 | + legacy_source = render_source(legacy_kernel, legacy_render) | ||
| 343 | + legacy_kernel_name, _ = self.define_kernel( | ||
| 344 | + legacy_source, [template_node], legacy_kernel, None | ||
| 345 | + ) | ||
| 346 | + | ||
| 347 | + runtime_renderers = composite.runtime_renderer_factory(composite) | ||
| 348 | + tasklist_renderer = runtime_renderers["tasklist"] | ||
| 349 | + with tasklist_renderer.patch_runtime_args(): | ||
| 350 | + tasklist_source = render_source( | ||
| 351 | + tasklist_renderer.kernel, tasklist_renderer.render | ||
| 352 | + ) | ||
| 353 | + tasklist_kernel_name, _ = self.define_kernel( | ||
| 354 | + tasklist_source, [template_node], tasklist_renderer.kernel, None | ||
| 355 | + ) | ||
| 356 | + | ||
| 357 | + tasklist_no_split_renderer = runtime_renderers["tasklist_no_split"] | ||
| 358 | + with tasklist_no_split_renderer.patch_runtime_args(): | ||
| 359 | + tasklist_no_split_source = render_source( | ||
| 360 | + tasklist_no_split_renderer.kernel, | ||
| 361 | + tasklist_no_split_renderer.render, | ||
| 362 | + ) | ||
| 363 | + tasklist_no_split_kernel_name, _ = self.define_kernel( | ||
| 364 | + tasklist_no_split_source, | ||
| 365 | + [template_node], | ||
| 366 | + tasklist_no_split_renderer.kernel, | ||
| 367 | + None, | ||
| 368 | + ) | ||
| 369 | + | ||
| 370 | + reduce_renderer = runtime_renderers["reduce"] | ||
| 371 | + with reduce_renderer.patch_runtime_args(): | ||
| 372 | + reduce_source = render_source( | ||
| 373 | + reduce_renderer.kernel, reduce_renderer.render | ||
| 374 | + ) | ||
| 375 | + reduce_kernel_name, _ = self.define_kernel( | ||
| 376 | + reduce_source, [template_node], reduce_renderer.kernel, None | ||
| 377 | + ) | ||
| 378 | + | ||
| 379 | + template_node.mark_run() | ||
| 380 | + wrapper = V.graph.wrapper_code | ||
| 381 | + wrapper.write_triton_header_once() | ||
| 382 | + _, legacy_call_args, _, _ = legacy_kernel.args.python_argdefs() | ||
| 383 | + _, tasklist_call_args, _, _ = tasklist_renderer.python_argdefs() | ||
| 384 | + _, tasklist_no_split_call_args, _, _ = ( | ||
| 385 | + tasklist_no_split_renderer.python_argdefs() | ||
| 386 | + ) | ||
| 387 | + _, reduce_call_args, _, _ = reduce_renderer.python_argdefs() | ||
| 388 | + runtime_arg_names = { | ||
| 389 | + **tasklist_renderer.runtime_arg_names, | ||
| 390 | + **tasklist_no_split_renderer.runtime_arg_names, | ||
| 391 | + **reduce_renderer.runtime_arg_names, | ||
| 392 | + } | ||
| 393 | + wrapper.generate_flex_attention_dkdv_dispatch( | ||
| 394 | + dispatch_spec=composite.dispatch_spec, | ||
| 395 | + legacy_kernel_name=legacy_kernel_name, | ||
| 396 | + legacy_call_args=legacy_call_args, | ||
| 397 | + tasklist_kernel_name=tasklist_kernel_name, | ||
| 398 | + tasklist_call_args=tasklist_call_args, | ||
| 399 | + tasklist_no_split_kernel_name=tasklist_no_split_kernel_name, | ||
| 400 | + tasklist_no_split_call_args=tasklist_no_split_call_args, | ||
| 401 | + reduce_kernel_name=reduce_kernel_name, | ||
| 402 | + reduce_call_args=reduce_call_args, | ||
| 403 | + q_num_blocks_name=legacy_kernel.named_input_nodes[ | ||
| 404 | + "Q_NUM_BLKS" | ||
| 405 | + ].get_name(), | ||
| 406 | + full_q_num_blocks_name=legacy_kernel.named_input_nodes[ | ||
| 407 | + "FULL_Q_NUM_BLKS" | ||
| 408 | + ].get_name(), | ||
| 409 | + dk_name=legacy_kernel.named_input_nodes["DK"].get_name(), | ||
| 410 | + dv_name=legacy_kernel.named_input_nodes["DV"].get_name(), | ||
| 411 | + runtime_arg_names=runtime_arg_names, | ||
| 412 | + ) | ||
| 413 | + | ||
| 414 | + for kernel in ( | ||
| 415 | + legacy_kernel, | ||
| 416 | + tasklist_renderer.kernel, | ||
| 417 | + tasklist_no_split_renderer.kernel, | ||
| 418 | + reduce_renderer.kernel, | ||
| 419 | + ): | ||
| 420 | + V.graph.removed_buffers |= kernel.removed_buffers | ||
| 421 | + V.graph.inplaced_to_remove |= kernel.inplaced_to_remove | ||
| 422 | + self.codegen_comment([template_node]) | ||
| 423 | + self.scheduler.free_buffers() | ||
| 424 | + return None | ||
| 425 | + | ||
| 289 | def codegen_combo_kernel(self, combo_kernel_node): | 426 | def codegen_combo_kernel(self, combo_kernel_node): |
| 290 | subkernel_nodes = combo_kernel_node.get_subkernel_nodes() | 427 | subkernel_nodes = combo_kernel_node.get_subkernel_nodes() |
| 291 | custom_part_algorithm = combo_kernel_node.use_custom_partition_algo | 428 | custom_part_algorithm = combo_kernel_node.use_custom_partition_algo |
| @@ -382,17 +519,21 @@ class NPUTritonScheduling(TritonScheduling): | |||
| 382 | src_code = src_code.replace('TRACED_GRAPH_HASH', traced_graph_hash) | 519 | src_code = src_code.replace('TRACED_GRAPH_HASH', traced_graph_hash) |
| 383 | src_code = src_code.replace('TRACED_GRAPH_DIR', npu_config.traced_fx_graph_cache) | 520 | src_code = src_code.replace('TRACED_GRAPH_DIR', npu_config.traced_fx_graph_cache) |
| 384 | else: | 521 | else: |
| 385 | - fused_name = ( | 522 | + kernel_name = getattr(kernel, "_npu_codegen_kernel_name", None) |
| 386 | - get_fused_kernel_name(node_schedule, config.triton.descriptive_names) | 523 | + if kernel_name is None: |
| 387 | - if config.triton.descriptive_names | 524 | + fused_name = ( |
| 388 | - else "" | 525 | + get_fused_kernel_name(node_schedule, config.triton.descriptive_names) |
| 389 | - ) | 526 | + if config.triton.descriptive_names |
| 390 | - if len(fused_name) > 35: | 527 | + else "" |
| 391 | - fused_name = fused_name[0:35] | 528 | + ) |
| 392 | - kernel_category = get_kernel_category_by_source_code(src_code)[:3] | 529 | + if len(fused_name) > 35: |
| 393 | - kernel_name = "_".join( | 530 | + fused_name = fused_name[0:35] |
| 394 | - ["triton", kernel_category, fused_name, wrapper.next_kernel_suffix()] | 531 | + kernel_category = get_kernel_category_by_source_code(src_code)[:3] |
| 395 | - ) | 532 | + kernel_name = "_".join( |
| 533 | + ["triton", kernel_category, fused_name, wrapper.next_kernel_suffix()] | ||
| 534 | + ) | ||
| 535 | + else: | ||
| 536 | + wrapper.next_kernel_suffix() | ||
| 396 | # use the original src_code as the key | 537 | # use the original src_code as the key |
| 397 | wrapper.src_to_kernel[kernel_cache_key] = kernel_name | 538 | wrapper.src_to_kernel[kernel_cache_key] = kernel_name |
| 398 | subs_name = kernel_name if config.triton.unique_kernel_names else "triton_" | 539 | subs_name = kernel_name if config.triton.unique_kernel_names else "triton_" |
| @@ -586,6 +727,14 @@ class NPUTritonScheduling(TritonScheduling): | |||
| 586 | _, (numel2, rnumel2) = node2.group | 727 | _, (numel2, rnumel2) = node2.group |
| 587 | why = WhyNoFuse(node1, node2) | 728 | why = WhyNoFuse(node1, node2) |
| 588 | 729 | ||
| 730 | + for node in (node1, node2): | ||
| 731 | + if node.is_template() and isinstance( | ||
| 732 | + node.get_template_node(), | ||
| 733 | + NPUFlexAttentionDkdvTemplateBuffer, | ||
| 734 | + ): | ||
| 735 | + why("composite dK/dV templates do not support fusion") | ||
| 736 | + return False | ||
| 737 | + | ||
| 589 | if node1.is_split_scan() and not node2.is_split_scan(): | 738 | if node1.is_split_scan() and not node2.is_split_scan(): |
| 590 | if node2.is_reduction(): | 739 | if node2.is_reduction(): |
| 591 | why("Split scan cannot fuse with reductions") | 740 | why("Split scan cannot fuse with reductions") |
| @@ -15,7 +15,12 @@ from torch._inductor.utils import ( | |||
| 15 | from torch._inductor.virtualized import V | 15 | from torch._inductor.virtualized import V |
| 16 | from torch._inductor.ir import GraphPartitionSignature, TorchBindObject, NoneLayout | 16 | from torch._inductor.ir import GraphPartitionSignature, TorchBindObject, NoneLayout |
| 17 | from torch._dynamo.utils import counters | 17 | from torch._dynamo.utils import counters |
| 18 | -from torch._inductor.codegen.common import DeferredLine, WorkspaceArg, IndentedBuffer | 18 | +from torch._inductor.codegen.common import ( |
| 19 | + DeferredLine, | ||
| 20 | + DeferredLineBase, | ||
| 21 | + WorkspaceArg, | ||
| 22 | + IndentedBuffer, | ||
| 23 | +) | ||
| 19 | from torch._inductor.codegen.wrapper import BufferLike, WrapperLine | 24 | from torch._inductor.codegen.wrapper import BufferLike, WrapperLine |
| 20 | from torch._inductor import ir | 25 | from torch._inductor import ir |
| 21 | import torch_npu.npu.aclnn | 26 | import torch_npu.npu.aclnn |
| @@ -25,6 +30,26 @@ from torch_npu._inductor._aclgraph_update_plan import ( | |||
| 25 | emit_inductor_aclgraph_update_plan_for_wrapper, | 30 | emit_inductor_aclgraph_update_plan_for_wrapper, |
| 26 | ) | 31 | ) |
| 27 | from torch_npu._inductor.utils import resolve_npu_device_index | 32 | from torch_npu._inductor.utils import resolve_npu_device_index |
| 33 | +from ..flex_attention_tasklist import DKDV_TASKLIST_HELPER_SOURCE | ||
| 34 | + | ||
| 35 | + | ||
| 36 | +class _RuntimeHelperDefinitionsLine(DeferredLineBase): | ||
| 37 | + def __init__(self, definitions: IndentedBuffer, prefix: str = ""): | ||
| 38 | + super().__init__(prefix) | ||
| 39 | + self.definitions = definitions | ||
| 40 | + | ||
| 41 | + def __call__(self): | ||
| 42 | + source = self.definitions.getvalue() | ||
| 43 | + if not source: | ||
| 44 | + return None | ||
| 45 | + if not self.line: | ||
| 46 | + return source | ||
| 47 | + return "\n".join( | ||
| 48 | + f"{self.line}{line}" if line else line for line in source.splitlines() | ||
| 49 | + ) | ||
| 50 | + | ||
| 51 | + def _new_line(self, line): | ||
| 52 | + return _RuntimeHelperDefinitionsLine(self.definitions, line) | ||
| 28 | 53 | ||
| 29 | 54 | ||
| 30 | def _is_codegen_graph_partition_subgraph(wrapper) -> bool: | 55 | def _is_codegen_graph_partition_subgraph(wrapper) -> bool: |
| @@ -173,12 +198,156 @@ class NPUMultiOutputLine(MultiOutputLine): | |||
| 173 | 198 | ||
| 174 | class NPUPythonWrapperCodeGen(_NPUKernelCodegenMixin, PythonWrapperCodegen): | 199 | class NPUPythonWrapperCodeGen(_NPUKernelCodegenMixin, PythonWrapperCodegen): |
| 175 | def __init__(self): | 200 | def __init__(self): |
| 201 | + self.runtime_helper_definitions = IndentedBuffer() | ||
| 202 | + self._flex_attention_dkdv_tasklist_helpers_written = False | ||
| 176 | super().__init__() | 203 | super().__init__() |
| 177 | self.buffer_args_multi_stream_intent = {} | 204 | self.buffer_args_multi_stream_intent = {} |
| 178 | self.buffer_define_multi_stream = {} | 205 | self.buffer_define_multi_stream = {} |
| 179 | self.extern_node_intent_multi_stream = [] | 206 | self.extern_node_intent_multi_stream = [] |
| 180 | self.pre_define_buffer = [] | 207 | self.pre_define_buffer = [] |
| 181 | 208 | ||
| 209 | + def write_async_compile_wait(self) -> None: | ||
| 210 | + super().write_async_compile_wait() | ||
| 211 | + self.prefix.writeline( | ||
| 212 | + _RuntimeHelperDefinitionsLine(self.runtime_helper_definitions) | ||
| 213 | + ) | ||
| 214 | + | ||
| 215 | + def write_flex_attention_dkdv_tasklist_helpers_once(self) -> None: | ||
| 216 | + if self._flex_attention_dkdv_tasklist_helpers_written: | ||
| 217 | + return | ||
| 218 | + self.runtime_helper_definitions.splice(DKDV_TASKLIST_HELPER_SOURCE) | ||
| 219 | + self.runtime_helper_definitions.writeline("") | ||
| 220 | + self._flex_attention_dkdv_tasklist_helpers_written = True | ||
| 221 | + | ||
| 222 | + def generate_flex_attention_dkdv_dispatch( | ||
| 223 | + self, | ||
| 224 | + *, | ||
| 225 | + dispatch_spec, | ||
| 226 | + legacy_kernel_name, | ||
| 227 | + legacy_call_args, | ||
| 228 | + tasklist_kernel_name, | ||
| 229 | + tasklist_call_args, | ||
| 230 | + tasklist_no_split_kernel_name, | ||
| 231 | + tasklist_no_split_call_args, | ||
| 232 | + reduce_kernel_name, | ||
| 233 | + reduce_call_args, | ||
| 234 | + q_num_blocks_name, | ||
| 235 | + full_q_num_blocks_name, | ||
| 236 | + dk_name, | ||
| 237 | + dv_name, | ||
| 238 | + runtime_arg_names, | ||
| 239 | + ) -> None: | ||
| 240 | + self.write_flex_attention_dkdv_tasklist_helpers_once() | ||
| 241 | + suffix = self.next_kernel_suffix() | ||
| 242 | + prefix = f"dkdv_tasklist_{suffix}" | ||
| 243 | + use_tasklist = f"{prefix}_use_tasklist" | ||
| 244 | + max_sub = f"{prefix}_max_sub" | ||
| 245 | + work_items_tensor = f"{prefix}_work_items_t" | ||
| 246 | + task_offsets_tensor = f"{prefix}_task_offsets_t" | ||
| 247 | + split_bases_tensor = f"{prefix}_split_bases_t" | ||
| 248 | + dk_partial = f"{prefix}_dk_partial" | ||
| 249 | + dv_partial = f"{prefix}_dv_partial" | ||
| 250 | + num_split_bases = f"{prefix}_num_split_bases" | ||
| 251 | + | ||
| 252 | + device = V.graph.get_current_device_or_throw() | ||
| 253 | + stream = PythonWrapperCodegen.write_get_raw_stream( | ||
| 254 | + self, device.index, V.graph | ||
| 255 | + ) | ||
| 256 | + self.writeline( | ||
| 257 | + f"{use_tasklist}, {work_items_tensor}, {task_offsets_tensor}, " | ||
| 258 | + f"{split_bases_tensor}, {max_sub} = " | ||
| 259 | + f"_get_or_build_dkdv_task_list(" | ||
| 260 | + f"{q_num_blocks_name}, {full_q_num_blocks_name}, " | ||
| 261 | + f"{dispatch_spec.batch_size}, " | ||
| 262 | + f"{dispatch_spec.num_kv_heads}, {dispatch_spec.num_kv_blocks}, " | ||
| 263 | + f"{dispatch_spec.sparse_kv_multiple}, " | ||
| 264 | + f"{dispatch_spec.launch_programs}, {dk_name}.device)" | ||
| 265 | + ) | ||
| 266 | + self.writeline(f"if {use_tasklist}:") | ||
| 267 | + self.writeline(f" {num_split_bases} = {split_bases_tensor}.size(0)") | ||
| 268 | + self.writeline(f" if {num_split_bases} > 0:") | ||
| 269 | + self.writeline( | ||
| 270 | + f" {dk_partial} = torch.empty_strided(" | ||
| 271 | + f"({max_sub}, *{dk_name}.size()), " | ||
| 272 | + f"({dispatch_spec.partial_dk_stride}, *{dk_name}.stride()), " | ||
| 273 | + f"dtype=torch.float32, device={dk_name}.device)" | ||
| 274 | + ) | ||
| 275 | + self.writeline(f" {dk_partial}.zero_()") | ||
| 276 | + self.writeline( | ||
| 277 | + f" {dv_partial} = torch.empty_strided(" | ||
| 278 | + f"({max_sub}, *{dv_name}.size()), " | ||
| 279 | + f"({dispatch_spec.partial_dv_stride}, *{dv_name}.stride()), " | ||
| 280 | + f"dtype=torch.float32, device={dv_name}.device)" | ||
| 281 | + ) | ||
| 282 | + self.writeline(f" {dv_partial}.zero_()") | ||
| 283 | + self.writeline(" else:") | ||
| 284 | + self.writeline(f" {split_bases_tensor} = None") | ||
| 285 | + self.writeline(f" {dk_partial} = {dk_name}") | ||
| 286 | + self.writeline(f" {dv_partial} = {dv_name}") | ||
| 287 | + | ||
| 288 | + runtime_values = { | ||
| 289 | + "work_items_t": work_items_tensor, | ||
| 290 | + "task_offsets_t": task_offsets_tensor, | ||
| 291 | + "split_bases_t": split_bases_tensor, | ||
| 292 | + "dk_partial": dk_partial, | ||
| 293 | + "dv_partial": dv_partial, | ||
| 294 | + } | ||
| 295 | + | ||
| 296 | + def replace_runtime_args(call_args): | ||
| 297 | + replacements = { | ||
| 298 | + outer_name: runtime_values[wrapper_name] | ||
| 299 | + for outer_name, wrapper_name in runtime_arg_names.items() | ||
| 300 | + } | ||
| 301 | + return [replacements.get(str(arg), arg) for arg in call_args] | ||
| 302 | + | ||
| 303 | + tasklist_args = replace_runtime_args(tasklist_call_args) | ||
| 304 | + tasklist_grid = [dispatch_spec.launch_programs, 1, 1] | ||
| 305 | + tasklist_args_text = ", ".join( | ||
| 306 | + self.prepare_triton_kernel_call([*tasklist_args, *tasklist_grid]) | ||
| 307 | + ) | ||
| 308 | + tasklist_no_split_args = replace_runtime_args( | ||
| 309 | + tasklist_no_split_call_args | ||
| 310 | + ) | ||
| 311 | + tasklist_no_split_args_text = ", ".join( | ||
| 312 | + self.prepare_triton_kernel_call( | ||
| 313 | + [*tasklist_no_split_args, *tasklist_grid] | ||
| 314 | + ) | ||
| 315 | + ) | ||
| 316 | + self.writeline(f" if {num_split_bases} > 0:") | ||
| 317 | + self.writeline( | ||
| 318 | + f" {tasklist_kernel_name}.run(" | ||
| 319 | + f"{tasklist_args_text}, stream={stream})" | ||
| 320 | + ) | ||
| 321 | + self.writeline(" else:") | ||
| 322 | + self.writeline( | ||
| 323 | + f" {tasklist_no_split_kernel_name}.run(" | ||
| 324 | + f"{tasklist_no_split_args_text}, stream={stream})" | ||
| 325 | + ) | ||
| 326 | + | ||
| 327 | + reduce_args = replace_runtime_args(reduce_call_args) | ||
| 328 | + reduce_args_text = ", ".join( | ||
| 329 | + self.prepare_triton_kernel_call( | ||
| 330 | + [*reduce_args, num_split_bases, 1, 1] | ||
| 331 | + ) | ||
| 332 | + ) | ||
| 333 | + self.writeline(f" if {num_split_bases} > 0:") | ||
| 334 | + self.writeline( | ||
| 335 | + f" {reduce_kernel_name}.run(" | ||
| 336 | + f"{reduce_args_text}, stream={stream})" | ||
| 337 | + ) | ||
| 338 | + self.writeline( | ||
| 339 | + f" del {work_items_tensor}, {task_offsets_tensor}, " | ||
| 340 | + f"{split_bases_tensor}, {dk_partial}, {dv_partial}" | ||
| 341 | + ) | ||
| 342 | + self.writeline("else:") | ||
| 343 | + legacy_grid = [dispatch_spec.launch_programs, 1, 1] | ||
| 344 | + legacy_args_text = ", ".join( | ||
| 345 | + self.prepare_triton_kernel_call([*legacy_call_args, *legacy_grid]) | ||
| 346 | + ) | ||
| 347 | + self.writeline( | ||
| 348 | + f" {legacy_kernel_name}.run({legacy_args_text}, stream={stream})" | ||
| 349 | + ) | ||
| 350 | + | ||
| 182 | 351 | ||
| 183 | 352 | ||
| 184 | def _get_triton_info_kernel_cls(cls): | 353 | def _get_triton_info_kernel_cls(cls): |
| @@ -1,6 +1,7 @@ | |||
| 1 | import logging | 1 | import logging |
| 2 | import os # noqa: C101 | 2 | import os # noqa: C101 |
| 3 | import re | 3 | import re |
| 4 | +from typing import Optional | ||
| 4 | import sys | 5 | import sys |
| 5 | 6 | ||
| 6 | import torch | 7 | import torch |
| @@ -179,6 +180,11 @@ inductor_config.triton.mix_order_reduction = False | |||
| 179 | inductor_config.loop_reindexing_after_fusion = False | 180 | inductor_config.loop_reindexing_after_fusion = False |
| 180 | 181 | ||
| 181 | 182 | ||
| 183 | +def _read_env_bool(name: str, default: str = "False") -> bool: | ||
| 184 | + value = os.environ.get(name, default) | ||
| 185 | + return value.strip().lower() in ("1", "true", "yes", "on") | ||
| 186 | + | ||
| 187 | + | ||
| 182 | # Enable the SIMT Welford lowering for variance and layer normalization. | 188 | # Enable the SIMT Welford lowering for variance and layer normalization. |
| 183 | # Keep it disabled by default while the new path is being rolled out. | 189 | # Keep it disabled by default while the new path is being rolled out. |
| 184 | enable_welford = os.getenv("TORCHINDUCTOR_ENABLE_WELFORD", "0") == "1" | 190 | enable_welford = os.getenv("TORCHINDUCTOR_ENABLE_WELFORD", "0") == "1" |
| @@ -354,6 +360,9 @@ simt_default_warp_stacksize = 256 * 32 | |||
| 354 | default_nddma_switch = "1" if is_ascend950 else "0" | 360 | default_nddma_switch = "1" if is_ascend950 else "0" |
| 355 | nddma_switch = os.getenv("TORCHINDUCTOR_NDDMA", default_nddma_switch) == "1" | 361 | nddma_switch = os.getenv("TORCHINDUCTOR_NDDMA", default_nddma_switch) == "1" |
| 356 | enable_fast_gelu = os.getenv("TORCHINDUCTOR_ENABLE_FAST_GELU", "0") == "1" | 362 | enable_fast_gelu = os.getenv("TORCHINDUCTOR_ENABLE_FAST_GELU", "0") == "1" |
| 363 | +enable_flex_attention_dq_before_scale_materialize = os.environ.get( | ||
| 364 | + "FLEX_ATTENTION_DQ_BEFORE_SCALE_MATERIALIZE", "1" | ||
| 365 | +).lower() in ("1", "true", "yes") | ||
| 357 | 366 | ||
| 358 | aggresive_autotune = os.getenv("INDUCTOR_ASCEND_AGGRESSIVE_AUTOTUNE", "0").lower() in ( | 367 | aggresive_autotune = os.getenv("INDUCTOR_ASCEND_AGGRESSIVE_AUTOTUNE", "0").lower() in ( |
| 359 | "1", | 368 | "1", |
| @@ -421,3 +430,155 @@ autotune_continue_on_failure = os.environ.get('TORCHINDUCTOR_NPU_BACKEND') == "d | |||
| 421 | enable_fused_matmul_relu = _parse_bool_env( | 430 | enable_fused_matmul_relu = _parse_bool_env( |
| 422 | "TORCHINDUCTOR_ENABLE_FUSED_MATMUL_RELU", False | 431 | "TORCHINDUCTOR_ENABLE_FUSED_MATMUL_RELU", False |
| 423 | ) | 432 | ) |
| 433 | + | ||
| 434 | +FLEX_ATTENTION_NPU_COMPILE_HINT_KEYS = ( | ||
| 435 | + "limit_auto_multi_buffer_buffer", | ||
| 436 | + "multibuffer", | ||
| 437 | + "unit_flag", | ||
| 438 | + "enable_ubuf_saving", | ||
| 439 | + "hfusion_enable_multiple_consumer_fusion", | ||
| 440 | + "enable_select_analysis", | ||
| 441 | + "limit_auto_multi_buffer_only_for_local_buffer", | ||
| 442 | + "limit_auto_multi_buffer_of_local_buffer", | ||
| 443 | + "set_workspace_multibuffer", | ||
| 444 | + "tile_mix_vector_loop", | ||
| 445 | + "tile_mix_cube_loop", | ||
| 446 | + "enable_dynamic_cv_pipeline", | ||
| 447 | + "intra_cache_num", | ||
| 448 | + "inter_cache_num", | ||
| 449 | + "enable_cross_if_fusion", | ||
| 450 | + "enable_buffer_insert_optimization", | ||
| 451 | + "enable_ub_refine_opt", | ||
| 452 | +) | ||
| 453 | + | ||
| 454 | + | ||
| 455 | +class flex_attention: | ||
| 456 | + enable_npu_optimization = False | ||
| 457 | + use_config_generator = True | ||
| 458 | + metadata_auto_infer = True | ||
| 459 | + flexattention_mask_out = True | ||
| 460 | + # Keep rollout disabled until generated outputcode and NPU numerics have | ||
| 461 | + # been reviewed. Unsupported graphs always retain the legacy dK/dV path. | ||
| 462 | + bwd_dkdv_tasklist = True | ||
| 463 | + | ||
| 464 | + multibuffer = True | ||
| 465 | + unit_flag = True | ||
| 466 | + enable_ubuf_saving = True | ||
| 467 | + limit_auto_multi_buffer_buffer = "no-limit" | ||
| 468 | + hfusion_enable_multiple_consumer_fusion = True | ||
| 469 | + enable_select_analysis = False | ||
| 470 | + limit_auto_multi_buffer_only_for_local_buffer = False | ||
| 471 | + limit_auto_multi_buffer_of_local_buffer = "no-limit" | ||
| 472 | + set_workspace_multibuffer = 4 | ||
| 473 | + tile_mix_vector_loop = 4 | ||
| 474 | + tile_mix_cube_loop = 4 | ||
| 475 | + enable_dynamic_cv_pipeline = False | ||
| 476 | + | ||
| 477 | + bwd_dq_limit_auto_multi_buffer_of_local_buffer = "no-l0c" | ||
| 478 | + bwd_dkdv_limit_auto_multi_buffer_of_local_buffer = "no-l0c" | ||
| 479 | + | ||
| 480 | + enable_buffer_insert_optimization = False | ||
| 481 | + enable_ub_refine_opt = False | ||
| 482 | + | ||
| 483 | + | ||
| 484 | + def _filter_compile_options_for_soc(cls, options: dict) -> dict: | ||
| 485 | + options = options.copy() | ||
| 486 | + if is_ascend950: | ||
| 487 | + options.pop("enable_dynamic_cv_pipeline", None) | ||
| 488 | + return options | ||
| 489 | + | ||
| 490 | + | ||
| 491 | + def _compile_options(cls, keys, overrides: Optional[dict] = None) -> dict: | ||
| 492 | + options = {key: getattr(cls, key) for key in keys} | ||
| 493 | + if overrides: | ||
| 494 | + options.update(overrides) | ||
| 495 | + return cls._filter_compile_options_for_soc(options) | ||
| 496 | + | ||
| 497 | + | ||
| 498 | + def get_npu_compile_hint_params(cls) -> dict: | ||
| 499 | + return cls._compile_options(FLEX_ATTENTION_NPU_COMPILE_HINT_KEYS) | ||
| 500 | + | ||
| 501 | + | ||
| 502 | + def get_sparse_mask_cvpipeline_compile_options( | ||
| 503 | + cls, | ||
| 504 | + *, | ||
| 505 | + enabled: bool, | ||
| 506 | + tile_mix_loop: int, | ||
| 507 | + enable_compile_hint: bool, | ||
| 508 | + ) -> dict: | ||
| 509 | + return cls._compile_options( | ||
| 510 | + ( | ||
| 511 | + "enable_ubuf_saving", | ||
| 512 | + "unit_flag", | ||
| 513 | + "set_workspace_multibuffer", | ||
| 514 | + "limit_auto_multi_buffer_buffer", | ||
| 515 | + "hfusion_enable_multiple_consumer_fusion", | ||
| 516 | + ), | ||
| 517 | + overrides={ | ||
| 518 | + "multibuffer": enabled, | ||
| 519 | + "limit_auto_multi_buffer_only_for_local_buffer": not enabled, | ||
| 520 | + "tile_mix_vector_loop": tile_mix_loop, | ||
| 521 | + "tile_mix_cube_loop": tile_mix_loop, | ||
| 522 | + "ENABLE_COMPILE_HINT": enable_compile_hint if enabled else False, | ||
| 523 | + "intra_cache_num": 3, | ||
| 524 | + "inter_cache_num": 2, | ||
| 525 | + "enable_cross_if_fusion": True, | ||
| 526 | + "enable_buffer_insert_optimization": True, | ||
| 527 | + "enable_ub_refine_opt": True, | ||
| 528 | + }, | ||
| 529 | + ) | ||
| 530 | + | ||
| 531 | + | ||
| 532 | + def get_bwd_dq_compile_options(cls) -> dict: | ||
| 533 | + return cls._compile_options( | ||
| 534 | + ( | ||
| 535 | + "limit_auto_multi_buffer_buffer", | ||
| 536 | + "hfusion_enable_multiple_consumer_fusion", | ||
| 537 | + "enable_select_analysis", | ||
| 538 | + ), | ||
| 539 | + overrides={ | ||
| 540 | + "limit_auto_multi_buffer_of_local_buffer": ( | ||
| 541 | + cls.bwd_dq_limit_auto_multi_buffer_of_local_buffer | ||
| 542 | + ), | ||
| 543 | + "intra_cache_num": 3, | ||
| 544 | + "inter_cache_num": 2, | ||
| 545 | + }, | ||
| 546 | + ) | ||
| 547 | + | ||
| 548 | + | ||
| 549 | + def get_bwd_dkdv_compile_options(cls) -> dict: | ||
| 550 | + return cls._compile_options( | ||
| 551 | + ( | ||
| 552 | + "limit_auto_multi_buffer_buffer", | ||
| 553 | + "hfusion_enable_multiple_consumer_fusion", | ||
| 554 | + "unit_flag", | ||
| 555 | + "enable_dynamic_cv_pipeline", | ||
| 556 | + ), | ||
| 557 | + overrides={ | ||
| 558 | + "limit_auto_multi_buffer_of_local_buffer": ( | ||
| 559 | + cls.bwd_dkdv_limit_auto_multi_buffer_of_local_buffer | ||
| 560 | + ), | ||
| 561 | + "intra_cache_num": 2, | ||
| 562 | + "inter_cache_num": 1, | ||
| 563 | + }, | ||
| 564 | + ) | ||
| 565 | + | ||
| 566 | + | ||
| 567 | +flex_attention.bwd_dkdv_tasklist = _read_env_bool( | ||
| 568 | + "TORCHINDUCTOR_ASCEND_FLEX_ATTENTION_BWD_DKDV_TASKLIST", | ||
| 569 | + "1" if flex_attention.bwd_dkdv_tasklist else "0", | ||
| 570 | +) | ||
| 571 | +flex_attention.flexattention_mask_out = _read_env_bool( | ||
| 572 | + "TORCHINDUCTOR_FLEXATTENTION_MASKOUT", | ||
| 573 | + "1" if flex_attention.flexattention_mask_out else "0", | ||
| 574 | +) | ||
| 575 | + | ||
| 576 | + | ||
| 577 | +def apply_flex_attention_npu_params(config: dict, *, enable: bool) -> dict: | ||
| 578 | + config = config.copy() | ||
| 579 | + if enable: | ||
| 580 | + config.update(flex_attention.get_npu_compile_hint_params()) | ||
| 581 | + config["ENABLE_COMPILE_HINT"] = True | ||
| 582 | + else: | ||
| 583 | + config["ENABLE_COMPILE_HINT"] = False | ||
| 584 | + return config | ||
| @@ -0,0 +1,329 @@ | |||
| 1 | +import inspect | ||
| 2 | +import math | ||
| 3 | +import re | ||
| 4 | +import textwrap | ||
| 5 | +from dataclasses import dataclass | ||
| 6 | + | ||
| 7 | +import torch | ||
| 8 | + | ||
| 9 | + | ||
| 10 | + | ||
| 11 | +class RuntimeTemplateArg: | ||
| 12 | + name: str | ||
| 13 | + dtype: torch.dtype | ||
| 14 | + rank: int | ||
| 15 | + wrapper_name: str | ||
| 16 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | +class FlexAttentionDkdvDispatchSpec: | ||
| 20 | + launch_programs: int | ||
| 21 | + batch_size: int | ||
| 22 | + num_kv_heads: int | ||
| 23 | + num_kv_blocks: int | ||
| 24 | + sparse_kv_multiple: int | ||
| 25 | + sparse_kv_block_size: int | ||
| 26 | + block_n1: int | ||
| 27 | + partial_dk_stride: int | ||
| 28 | + partial_dv_stride: int | ||
| 29 | + | ||
| 30 | + | ||
| 31 | +def is_dkdv_tasklist_codegen_compatible( | ||
| 32 | + *, | ||
| 33 | + cpp_wrapper, | ||
| 34 | + aot_mode, | ||
| 35 | + bq, | ||
| 36 | + bkv, | ||
| 37 | + sparse_z, | ||
| 38 | + sparse_hq, | ||
| 39 | + sparse_kv_block_size, | ||
| 40 | + block_n1, | ||
| 41 | + q_num_blocks_dtype, | ||
| 42 | + full_q_num_blocks_dtype, | ||
| 43 | + q_num_blocks_contiguous, | ||
| 44 | + full_q_num_blocks_contiguous, | ||
| 45 | + accum_dtype, | ||
| 46 | +): | ||
| 47 | + static_dimensions = (bq, bkv, sparse_z, sparse_hq) | ||
| 48 | + return ( | ||
| 49 | + not cpp_wrapper | ||
| 50 | + and not aot_mode | ||
| 51 | + and all(isinstance(value, int) for value in static_dimensions) | ||
| 52 | + and bq == 1 | ||
| 53 | + and bq == bkv | ||
| 54 | + and sparse_z == bq | ||
| 55 | + and sparse_hq == 1 | ||
| 56 | + and block_n1 > 0 | ||
| 57 | + and sparse_kv_block_size % block_n1 == 0 | ||
| 58 | + and q_num_blocks_dtype == torch.int32 | ||
| 59 | + and full_q_num_blocks_dtype == torch.int32 | ||
| 60 | + and q_num_blocks_contiguous | ||
| 61 | + and full_q_num_blocks_contiguous | ||
| 62 | + and accum_dtype == torch.float32 | ||
| 63 | + ) | ||
| 64 | + | ||
| 65 | + | ||
| 66 | +def compute_dkdv_sparse_weights(q_num_blks, full_q_num_blks): | ||
| 67 | + weights = (q_num_blks + full_q_num_blks).reshape(-1) | ||
| 68 | + return [ | ||
| 69 | + int(value) | ||
| 70 | + for value in weights.detach().to("cpu", dtype=torch.int64).tolist() | ||
| 71 | + ] | ||
| 72 | + | ||
| 73 | + | ||
| 74 | +def should_use_dkdv_tasklist( | ||
| 75 | + w_sparse, | ||
| 76 | + batch_size, | ||
| 77 | + num_kv_heads, | ||
| 78 | + num_kv_blocks, | ||
| 79 | + sparse_kv_multiple, | ||
| 80 | + num_core, | ||
| 81 | +): | ||
| 82 | + if ( | ||
| 83 | + not w_sparse | ||
| 84 | + or batch_size != 1 | ||
| 85 | + or num_core <= 0 | ||
| 86 | + or sparse_kv_multiple <= 0 | ||
| 87 | + ): | ||
| 88 | + return False | ||
| 89 | + | ||
| 90 | + total_base = batch_size * num_kv_heads * num_kv_blocks | ||
| 91 | + total_weight = num_kv_heads * sum(w_sparse) | ||
| 92 | + if total_base == 0 or total_weight == 0: | ||
| 93 | + return False | ||
| 94 | + | ||
| 95 | + mean_weight = total_weight / total_base | ||
| 96 | + full_rounds, tail_cores = divmod(total_base, num_core) | ||
| 97 | + has_significant_tail = ( | ||
| 98 | + tail_cores > 0 and full_rounds <= 2 and tail_cores / num_core < 0.5 | ||
| 99 | + ) | ||
| 100 | + has_weight_imbalance = ( | ||
| 101 | + tail_cores == 0 and max(w_sparse) / mean_weight > 1.5 | ||
| 102 | + ) | ||
| 103 | + return has_significant_tail or has_weight_imbalance | ||
| 104 | + | ||
| 105 | + | ||
| 106 | +def bin_pack_dkdv_hkv_continuous(work_items, num_core): | ||
| 107 | + bins = [[] for _ in range(num_core)] | ||
| 108 | + bin_weights = [0.0] * num_core | ||
| 109 | + groups = {} | ||
| 110 | + for item in work_items: | ||
| 111 | + groups.setdefault(item[0], []).append(item) | ||
| 112 | + | ||
| 113 | + for kv_head in sorted(groups): | ||
| 114 | + group = sorted( | ||
| 115 | + groups[kv_head], key=lambda item: item[5], reverse=True | ||
| 116 | + ) | ||
| 117 | + for item in group: | ||
| 118 | + lightest = bin_weights.index(min(bin_weights)) | ||
| 119 | + bins[lightest].append(item) | ||
| 120 | + bin_weights[lightest] += item[5] | ||
| 121 | + return bins | ||
| 122 | + | ||
| 123 | + | ||
| 124 | +def build_dkdv_task_list( | ||
| 125 | + w_sparse, | ||
| 126 | + batch_size, | ||
| 127 | + num_kv_heads, | ||
| 128 | + num_kv_blocks, | ||
| 129 | + sparse_kv_multiple, | ||
| 130 | + num_core, | ||
| 131 | +): | ||
| 132 | + target = max( | ||
| 133 | + num_kv_heads * sum(w_sparse) / max(num_core, 1), | ||
| 134 | + 1.0, | ||
| 135 | + ) | ||
| 136 | + target_int = max(int(target), 1) | ||
| 137 | + weights_per_kv_block = [ | ||
| 138 | + int(w_sparse[kv_block // sparse_kv_multiple]) | ||
| 139 | + for kv_block in range(num_kv_blocks) | ||
| 140 | + ] | ||
| 141 | + template_items = [] | ||
| 142 | + template_split_bases = [] | ||
| 143 | + max_sub = 1 | ||
| 144 | + for kv_block, weight in enumerate(weights_per_kv_block): | ||
| 145 | + if weight == 0: | ||
| 146 | + continue | ||
| 147 | + if weight <= target: | ||
| 148 | + template_items.append( | ||
| 149 | + (kv_block, 0, 1, 0, float(weight)) | ||
| 150 | + ) | ||
| 151 | + continue | ||
| 152 | + | ||
| 153 | + split_count = max(1, math.ceil(weight / target_int)) | ||
| 154 | + template_split_bases.append((kv_block, split_count)) | ||
| 155 | + max_sub = max(max_sub, split_count) | ||
| 156 | + split_weight = weight / split_count | ||
| 157 | + for sub_id in range(split_count): | ||
| 158 | + template_items.append( | ||
| 159 | + (kv_block, sub_id, split_count, 1, split_weight) | ||
| 160 | + ) | ||
| 161 | + | ||
| 162 | + weighted_items = [] | ||
| 163 | + split_bases = [] | ||
| 164 | + for batch_idx in range(batch_size): | ||
| 165 | + for kv_head in range(num_kv_heads): | ||
| 166 | + weighted_items.extend( | ||
| 167 | + (kv_head, *item) for item in template_items | ||
| 168 | + ) | ||
| 169 | + split_bases.extend( | ||
| 170 | + (kv_head, *item) | ||
| 171 | + for item in template_split_bases | ||
| 172 | + ) | ||
| 173 | + | ||
| 174 | + bins = bin_pack_dkdv_hkv_continuous(weighted_items, num_core) | ||
| 175 | + work_items = [] | ||
| 176 | + task_offsets = [0] | ||
| 177 | + for bin_items in bins: | ||
| 178 | + work_items.extend(item[:5] for item in bin_items) | ||
| 179 | + task_offsets.append(len(work_items)) | ||
| 180 | + return work_items, task_offsets, split_bases, max_sub | ||
| 181 | + | ||
| 182 | + | ||
| 183 | +def get_or_build_dkdv_task_list( | ||
| 184 | + q_num_blks, | ||
| 185 | + full_q_num_blks, | ||
| 186 | + batch_size, | ||
| 187 | + num_kv_heads, | ||
| 188 | + num_kv_blocks, | ||
| 189 | + sparse_kv_multiple, | ||
| 190 | + num_core, | ||
| 191 | + device, | ||
| 192 | +): | ||
| 193 | + cache_key = ( | ||
| 194 | + batch_size, | ||
| 195 | + num_kv_heads, | ||
| 196 | + num_kv_blocks, | ||
| 197 | + sparse_kv_multiple, | ||
| 198 | + num_core, | ||
| 199 | + device, | ||
| 200 | + ) | ||
| 201 | + try: | ||
| 202 | + q_num_blks_version = q_num_blks._version | ||
| 203 | + full_q_num_blks_version = full_q_num_blks._version | ||
| 204 | + except RuntimeError: | ||
| 205 | + q_num_blks_version = None | ||
| 206 | + full_q_num_blks_version = None | ||
| 207 | + | ||
| 208 | + cache = getattr(q_num_blks, "_npu_dkdv_tasklist_cache", None) | ||
| 209 | + if cache is not None: | ||
| 210 | + entry = cache.get(cache_key) | ||
| 211 | + if ( | ||
| 212 | + entry is not None | ||
| 213 | + and entry[0] is full_q_num_blks | ||
| 214 | + and entry[1] == q_num_blks_version | ||
| 215 | + and entry[2] == full_q_num_blks_version | ||
| 216 | + ): | ||
| 217 | + return entry[3] | ||
| 218 | + | ||
| 219 | + weights = compute_dkdv_sparse_weights(q_num_blks, full_q_num_blks) | ||
| 220 | + use_tasklist = should_use_dkdv_tasklist( | ||
| 221 | + weights, | ||
| 222 | + batch_size, | ||
| 223 | + num_kv_heads, | ||
| 224 | + num_kv_blocks, | ||
| 225 | + sparse_kv_multiple, | ||
| 226 | + num_core, | ||
| 227 | + ) | ||
| 228 | + if use_tasklist: | ||
| 229 | + work_items, task_offsets, split_bases, max_sub = ( | ||
| 230 | + build_dkdv_task_list( | ||
| 231 | + weights, | ||
| 232 | + batch_size, | ||
| 233 | + num_kv_heads, | ||
| 234 | + num_kv_blocks, | ||
| 235 | + sparse_kv_multiple, | ||
| 236 | + num_core, | ||
| 237 | + ) | ||
| 238 | + ) | ||
| 239 | + if work_items: | ||
| 240 | + work_items_tensor = torch.tensor( | ||
| 241 | + work_items, dtype=torch.int32, device=device | ||
| 242 | + ) | ||
| 243 | + else: | ||
| 244 | + work_items_tensor = torch.zeros( | ||
| 245 | + (0, 5), dtype=torch.int32, device=device | ||
| 246 | + ) | ||
| 247 | + task_offsets_tensor = torch.tensor( | ||
| 248 | + task_offsets, dtype=torch.int32, device=device | ||
| 249 | + ) | ||
| 250 | + if split_bases: | ||
| 251 | + split_bases_tensor = torch.tensor( | ||
| 252 | + split_bases, dtype=torch.int32, device=device | ||
| 253 | + ) | ||
| 254 | + else: | ||
| 255 | + split_bases_tensor = torch.zeros( | ||
| 256 | + (0, 3), dtype=torch.int32, device=device | ||
| 257 | + ) | ||
| 258 | + result = ( | ||
| 259 | + True, | ||
| 260 | + work_items_tensor, | ||
| 261 | + task_offsets_tensor, | ||
| 262 | + split_bases_tensor, | ||
| 263 | + max_sub, | ||
| 264 | + ) | ||
| 265 | + else: | ||
| 266 | + result = (False, None, None, None, 1) | ||
| 267 | + | ||
| 268 | + if q_num_blks_version is not None and full_q_num_blks_version is not None: | ||
| 269 | + if cache is None: | ||
| 270 | + cache = {} | ||
| 271 | + setattr(q_num_blks, "_npu_dkdv_tasklist_cache", cache) # noqa: B010 | ||
| 272 | + cache[cache_key] = ( | ||
| 273 | + full_q_num_blks, | ||
| 274 | + q_num_blks_version, | ||
| 275 | + full_q_num_blks_version, | ||
| 276 | + result, | ||
| 277 | + ) | ||
| 278 | + return result | ||
| 279 | + | ||
| 280 | + | ||
| 281 | +def _generated_helper_source(function, generated_name, replacements=()): | ||
| 282 | + source = textwrap.dedent(inspect.getsource(function)) | ||
| 283 | + definition = f"def {function.__name__}(" | ||
| 284 | + source = source.replace(definition, f"def {generated_name}(", 1) | ||
| 285 | + for old_name, new_name in replacements: | ||
| 286 | + source = re.sub( | ||
| 287 | + rf"(?<![\w]){re.escape(old_name)}\(", | ||
| 288 | + f"{new_name}(", | ||
| 289 | + source, | ||
| 290 | + ) | ||
| 291 | + return source | ||
| 292 | + | ||
| 293 | + | ||
| 294 | +DKDV_TASKLIST_HELPER_SOURCE = "\n\n".join( | ||
| 295 | + ( | ||
| 296 | + _generated_helper_source( | ||
| 297 | + compute_dkdv_sparse_weights, "_compute_dkdv_sparse_weights" | ||
| 298 | + ), | ||
| 299 | + _generated_helper_source( | ||
| 300 | + should_use_dkdv_tasklist, "_should_use_dkdv_tasklist" | ||
| 301 | + ), | ||
| 302 | + _generated_helper_source( | ||
| 303 | + bin_pack_dkdv_hkv_continuous, | ||
| 304 | + "_bin_pack_dkdv_hkv_continuous", | ||
| 305 | + ), | ||
| 306 | + _generated_helper_source( | ||
| 307 | + build_dkdv_task_list, | ||
| 308 | + "_build_dkdv_task_list", | ||
| 309 | + ( | ||
| 310 | + ( | ||
| 311 | + "bin_pack_dkdv_hkv_continuous", | ||
| 312 | + "_bin_pack_dkdv_hkv_continuous", | ||
| 313 | + ), | ||
| 314 | + ), | ||
| 315 | + ), | ||
| 316 | + _generated_helper_source( | ||
| 317 | + get_or_build_dkdv_task_list, | ||
| 318 | + "_get_or_build_dkdv_task_list", | ||
| 319 | + ( | ||
| 320 | + ( | ||
| 321 | + "compute_dkdv_sparse_weights", | ||
| 322 | + "_compute_dkdv_sparse_weights", | ||
| 323 | + ), | ||
| 324 | + ("should_use_dkdv_tasklist", "_should_use_dkdv_tasklist"), | ||
| 325 | + ("build_dkdv_task_list", "_build_dkdv_task_list"), | ||
| 326 | + ), | ||
| 327 | + ), | ||
| 328 | + ) | ||
| 329 | +) | ||
| @@ -1,4 +1,4 @@ | |||
| 1 | from .mm import _register_npu_inductor_mm, _register_npu_inductor_addmm | 1 | from .mm import _register_npu_inductor_mm, _register_npu_inductor_addmm |
| 2 | from .bmm import _register_npu_inductor_bmm | 2 | from .bmm import _register_npu_inductor_bmm |
| 3 | from .mm_grouped import _register_npu_inductor_grouped_mm | 3 | from .mm_grouped import _register_npu_inductor_grouped_mm |
| 4 | -from .flex_attention import _register_npu_inductor_flex_attention, _validate_device | 4 | +from .flex_attention import _register_npu_inductor_flex_attention, _validate_device, patch_flex_attention |
| @@ -0,0 +1,973 @@ | |||
| 1 | +""" | ||
| 2 | +Flex Attention Configuration Generator. | ||
| 3 | + | ||
| 4 | +This module provides FlexAttentionConfigGenerator class that dynamically | ||
| 5 | +generates candidate configurations for Flex Attention kernel autotuning. | ||
| 6 | +Similar to TileGenerator, it generates BLOCK_M/BLOCK_N combinations based | ||
| 7 | +on input shapes and hardware constraints. | ||
| 8 | +""" | ||
| 9 | + | ||
| 10 | +from dataclasses import dataclass | ||
| 11 | +from enum import Enum | ||
| 12 | +from typing import Optional, Union | ||
| 13 | + | ||
| 14 | +import torch | ||
| 15 | +from torch._inductor import config as inductor_config | ||
| 16 | + | ||
| 17 | +from .. import config as npu_config | ||
| 18 | + | ||
| 19 | +log = npu_config.log | ||
| 20 | + | ||
| 21 | + | ||
| 22 | +class FlexMode(Enum): | ||
| 23 | + """Operation mode for Flex Attention.""" | ||
| 24 | + FWD = "fwd" | ||
| 25 | + BWD = "bwd" | ||
| 26 | + | ||
| 27 | + | ||
| 28 | + | ||
| 29 | +class FlexAttentionConfig: | ||
| 30 | + """Configuration for Flex Attention kernel.""" | ||
| 31 | + block_m: int | ||
| 32 | + block_n: int | ||
| 33 | + num_warps: int | ||
| 34 | + num_stages: int | ||
| 35 | + npu_params: Optional[dict] = None | ||
| 36 | + | ||
| 37 | + def to_dict(self) -> dict: | ||
| 38 | + """Convert to dictionary format.""" | ||
| 39 | + result = { | ||
| 40 | + "BLOCK_M": self.block_m, | ||
| 41 | + "BLOCK_N": self.block_n, | ||
| 42 | + "num_warps": self.num_warps, | ||
| 43 | + "num_stages": self.num_stages, | ||
| 44 | + } | ||
| 45 | + if self.npu_params: | ||
| 46 | + result.update(self.npu_params) | ||
| 47 | + return result | ||
| 48 | + | ||
| 49 | + | ||
| 50 | +class FlexAttentionConfigGenerator: | ||
| 51 | + """ | ||
| 52 | + Generate candidate configurations for Flex Attention kernel. | ||
| 53 | + | ||
| 54 | + This class dynamically generates BLOCK_M/BLOCK_N combinations based on | ||
| 55 | + input shapes and hardware constraints, similar to how TileGenerator | ||
| 56 | + works for general NPU kernels. | ||
| 57 | + | ||
| 58 | + Key features: | ||
| 59 | + 1. Constraint-aware: Only generates configs that satisfy SPARSE constraints | ||
| 60 | + 2. Performance-oriented: Considers wave efficiency and UB constraints | ||
| 61 | + 3. Extensible: Supports num_warps/num_stages/NPU parameter variations | ||
| 62 | + | ||
| 63 | + Example: | ||
| 64 | + >>> generator = FlexAttentionConfigGenerator( | ||
| 65 | + ... query_shape=(1, 8, 1024, 128), | ||
| 66 | + ... key_shape=(1, 8, 1024, 128), | ||
| 67 | + ... sparse_q_block_size=128, | ||
| 68 | + ... sparse_kv_block_size=128, | ||
| 69 | + ... dtype=torch.float16, | ||
| 70 | + ... num_cube_core=24, | ||
| 71 | + ... mode=FlexMode.FWD, | ||
| 72 | + ... ) | ||
| 73 | + >>> configs = generator.generate_configs() | ||
| 74 | + >>> print(len(configs)) # 10-20 configs | ||
| 75 | + """ | ||
| 76 | + | ||
| 77 | + BLOCK_SIZE_CANDIDATES = [256, 128, 64, 32, 16] | ||
| 78 | + | ||
| 79 | + MAX_CONFIGS = 30 | ||
| 80 | + | ||
| 81 | + def __init__( | ||
| 82 | + self, | ||
| 83 | + query_shape: tuple, | ||
| 84 | + key_shape: tuple, | ||
| 85 | + sparse_q_block_size: int, | ||
| 86 | + sparse_kv_block_size: int, | ||
| 87 | + dtype: torch.dtype, | ||
| 88 | + num_cube_core: int, | ||
| 89 | + mode: FlexMode = FlexMode.FWD, | ||
| 90 | + ): | ||
| 91 | + """ | ||
| 92 | + Initialize the configuration generator. | ||
| 93 | + | ||
| 94 | + Args: | ||
| 95 | + query_shape: Shape of query tensor (batch, heads, seq_len_q, head_dim) | ||
| 96 | + key_shape: Shape of key tensor (batch, heads, seq_len_kv, head_dim) | ||
| 97 | + sparse_q_block_size: SPARSE_Q_BLOCK_SIZE constraint | ||
| 98 | + sparse_kv_block_size: SPARSE_KV_BLOCK_SIZE constraint | ||
| 99 | + dtype: Data type of tensors | ||
| 100 | + num_cube_core: Number of AICore (cube cores) available | ||
| 101 | + mode: Forward or backward mode | ||
| 102 | + """ | ||
| 103 | + self.batch_size = query_shape[0] | ||
| 104 | + self.num_heads = query_shape[1] | ||
| 105 | + self.seq_len_q = query_shape[2] | ||
| 106 | + self.head_dim = query_shape[3] | ||
| 107 | + self.seq_len_kv = key_shape[2] | ||
| 108 | + | ||
| 109 | + self.sparse_q_block_size = sparse_q_block_size | ||
| 110 | + self.sparse_kv_block_size = sparse_kv_block_size | ||
| 111 | + | ||
| 112 | + self.dtype = dtype | ||
| 113 | + self.dtype_bytes = self._get_dtype_bytes(dtype) | ||
| 114 | + | ||
| 115 | + self.num_cube_core = num_cube_core | ||
| 116 | + | ||
| 117 | + self.mode = mode | ||
| 118 | + | ||
| 119 | + self.valid_block_m = self._get_valid_block_sizes(sparse_q_block_size) | ||
| 120 | + self.valid_block_n = self._get_valid_block_sizes(sparse_kv_block_size) | ||
| 121 | + | ||
| 122 | + self.configs: list[FlexAttentionConfig] = [] | ||
| 123 | + | ||
| 124 | + def _get_dtype_bytes(self, dtype: torch.dtype) -> int: | ||
| 125 | + """Get bytes per element for dtype.""" | ||
| 126 | + dtype_bytes_map = { | ||
| 127 | + torch.float16: 2, | ||
| 128 | + torch.bfloat16: 2, | ||
| 129 | + torch.float32: 4, | ||
| 130 | + torch.int8: 1, | ||
| 131 | + torch.int16: 2, | ||
| 132 | + torch.int32: 4, | ||
| 133 | + } | ||
| 134 | + return dtype_bytes_map.get(dtype, 4) | ||
| 135 | + | ||
| 136 | + def _get_valid_block_sizes(self, sparse_block_size: int) -> list[int]: | ||
| 137 | + """ | ||
| 138 | + Get valid block sizes that divide SPARSE_BLOCK_SIZE. | ||
| 139 | + | ||
| 140 | + For SPARSE_BLOCK_SIZE=128, returns: [128, 64, 32, 16] | ||
| 141 | + For SPARSE_BLOCK_SIZE=64, returns: [64, 32, 16] | ||
| 142 | + """ | ||
| 143 | + valid_sizes = [] | ||
| 144 | + for size in self.BLOCK_SIZE_CANDIDATES: | ||
| 145 | + if sparse_block_size % size == 0: | ||
| 146 | + valid_sizes.append(size) | ||
| 147 | + return valid_sizes | ||
| 148 | + | ||
| 149 | + def generate_configs(self) -> list[dict]: | ||
| 150 | + """ | ||
| 151 | + Generate all candidate configurations. | ||
| 152 | + | ||
| 153 | + Returns: | ||
| 154 | + List of config dictionaries with BLOCK_M, BLOCK_N, num_warps, num_stages | ||
| 155 | + """ | ||
| 156 | + self.configs = [] | ||
| 157 | + | ||
| 158 | + self._generate_block_combinations() | ||
| 159 | + self._filter_by_ub_constraint() | ||
| 160 | + self._add_npu_params() | ||
| 161 | + self._limit_config_count() | ||
| 162 | + | ||
| 163 | + return [cfg.to_dict() for cfg in self.configs] | ||
| 164 | + | ||
| 165 | + def _generate_block_combinations(self): | ||
| 166 | + """Generate BLOCK_M x BLOCK_N combinations based on mode.""" | ||
| 167 | + if self.mode == FlexMode.FWD: | ||
| 168 | + self._generate_fwd_combinations() | ||
| 169 | + else: | ||
| 170 | + self._generate_bwd_combinations() | ||
| 171 | + | ||
| 172 | + def _generate_fwd_combinations(self): | ||
| 173 | + """ | ||
| 174 | + Generate forward pass configurations. | ||
| 175 | + | ||
| 176 | + Strategy: | ||
| 177 | + 1. Start with safe default config (16, 16) for persistent mode compatibility | ||
| 178 | + 2. Add configs with different BLOCK_M/BLOCK_N ratios for autotuning | ||
| 179 | + 3. Consider wave efficiency (programs per AICore) | ||
| 180 | + """ | ||
| 181 | + # Use (16, 16) as the default config to ensure compilation stability | ||
| 182 | + # This is especially important for persistent kernel mode which may have | ||
| 183 | + # stricter constraints on tile sizes | ||
| 184 | + default_m, default_n = 16, 16 | ||
| 185 | + | ||
| 186 | + if default_m in self.valid_block_m and default_n in self.valid_block_n: | ||
| 187 | + self.configs.append(FlexAttentionConfig( | ||
| 188 | + block_m=default_m, | ||
| 189 | + block_n=default_n, | ||
| 190 | + num_warps=4, | ||
| 191 | + num_stages=3, | ||
| 192 | + )) | ||
| 193 | + | ||
| 194 | + seen = {(default_m, default_n)} | ||
| 195 | + | ||
| 196 | + for block_m in self.valid_block_m: | ||
| 197 | + for block_n in self.valid_block_n: | ||
| 198 | + if (block_m, block_n) in seen: | ||
| 199 | + continue | ||
| 200 | + | ||
| 201 | + programs_m = (self.seq_len_q + block_m - 1) // block_m | ||
| 202 | + total_programs = programs_m * self.batch_size * self.num_heads | ||
| 203 | + wave_efficiency = total_programs / self.num_cube_core | ||
| 204 | + | ||
| 205 | + if wave_efficiency >= 0.5 or total_programs <= self.num_cube_core: | ||
| 206 | + self.configs.append(FlexAttentionConfig( | ||
| 207 | + block_m=block_m, | ||
| 208 | + block_n=block_n, | ||
| 209 | + num_warps=4, | ||
| 210 | + num_stages=3, | ||
| 211 | + )) | ||
| 212 | + seen.add((block_m, block_n)) | ||
| 213 | + | ||
| 214 | + def _generate_bwd_combinations(self): | ||
| 215 | + """ | ||
| 216 | + Generate backward pass configurations. | ||
| 217 | + | ||
| 218 | + Backward uses BLOCK_M1, BLOCK_N1, BLOCK_M2, BLOCK_N2. | ||
| 219 | + Constraint: BLOCK_N1 % BLOCK_M1 == 0 | ||
| 220 | + """ | ||
| 221 | + # Match the backward outputcode target tile first. dkdv and dq lowering | ||
| 222 | + # may still override their unused BLOCK_* dimensions independently. | ||
| 223 | + default_m, default_n = 64, 64 | ||
| 224 | + if default_m in self.valid_block_m and default_n in self.valid_block_n: | ||
| 225 | + self.configs.append(FlexAttentionConfig( | ||
| 226 | + block_m=default_m, | ||
| 227 | + block_n=default_n, | ||
| 228 | + num_warps=4, | ||
| 229 | + num_stages=1, | ||
| 230 | + )) | ||
| 231 | + | ||
| 232 | + seen = {(default_m, default_n)} | ||
| 233 | + | ||
| 234 | + for block_m1 in self.valid_block_m: | ||
| 235 | + for block_n1 in self.valid_block_n: | ||
| 236 | + if block_n1 % block_m1 != 0: | ||
| 237 | + continue | ||
| 238 | + | ||
| 239 | + if (block_m1, block_n1) in seen: | ||
| 240 | + continue | ||
| 241 | + | ||
| 242 | + self.configs.append(FlexAttentionConfig( | ||
| 243 | + block_m=block_m1, | ||
| 244 | + block_n=block_n1, | ||
| 245 | + num_warps=4, | ||
| 246 | + num_stages=1, | ||
| 247 | + )) | ||
| 248 | + seen.add((block_m1, block_n1)) | ||
| 249 | + | ||
| 250 | + def _filter_by_ub_constraint(self): | ||
| 251 | + """ | ||
| 252 | + Filter configs that exceed UB size limit. | ||
| 253 | + | ||
| 254 | + UB usage estimation: | ||
| 255 | + - Q block: BLOCK_M * head_dim * dtype_bytes | ||
| 256 | + - K block: BLOCK_N * head_dim * dtype_bytes | ||
| 257 | + - V block: BLOCK_N * head_dim * dtype_bytes | ||
| 258 | + - Acc buffer: BLOCK_M * BLOCK_N * 4 (float32) | ||
| 259 | + """ | ||
| 260 | + filtered = [] | ||
| 261 | + | ||
| 262 | + for cfg in self.configs: | ||
| 263 | + q_ub = cfg.block_m * self.head_dim * self.dtype_bytes | ||
| 264 | + k_ub = cfg.block_n * self.head_dim * self.dtype_bytes | ||
| 265 | + v_ub = cfg.block_n * self.head_dim * self.dtype_bytes | ||
| 266 | + acc_ub = cfg.block_m * cfg.block_n * 4 | ||
| 267 | + | ||
| 268 | + total_ub = q_ub + k_ub + v_ub + acc_ub | ||
| 269 | + | ||
| 270 | + if total_ub <= npu_config.ub_size * 0.8: | ||
| 271 | + filtered.append(cfg) | ||
| 272 | + | ||
| 273 | + self.configs = filtered if filtered else self.configs[:1] | ||
| 274 | + | ||
| 275 | + def _add_npu_params(self): | ||
| 276 | + """ | ||
| 277 | + Add NPU optimization parameters if enabled. | ||
| 278 | + | ||
| 279 | + Similar to TileGenerator.tune_multibuffer() | ||
| 280 | + """ | ||
| 281 | + log.info("[flex_attention] NPU optimization enabled: %s", npu_config.flex_attention.enable_npu_optimization) | ||
| 282 | + | ||
| 283 | + if not npu_config.flex_attention.enable_npu_optimization: | ||
| 284 | + # Even when NPU optimization is disabled, we need to set ENABLE_COMPILE_HINT | ||
| 285 | + # to avoid NameError in kernel code | ||
| 286 | + for cfg in self.configs: | ||
| 287 | + cfg.npu_params = npu_config.apply_flex_attention_npu_params( | ||
| 288 | + cfg.npu_params or {}, | ||
| 289 | + enable=False, | ||
| 290 | + ) | ||
| 291 | + return | ||
| 292 | + | ||
| 293 | + npu_params = npu_config.apply_flex_attention_npu_params( | ||
| 294 | + {}, | ||
| 295 | + enable=True, | ||
| 296 | + ) | ||
| 297 | + | ||
| 298 | + log.debug("NPU parameters: %s", npu_params) | ||
| 299 | + | ||
| 300 | + # Keep the original configs as a conservative fallback, then append the | ||
| 301 | + # NPU-tuned variants so autotuning can still fall back if the enhanced | ||
| 302 | + # path overflows UB or hits backend bugs. | ||
| 303 | + new_configs = [] | ||
| 304 | + for cfg in self.configs: | ||
| 305 | + cfg.npu_params = npu_config.apply_flex_attention_npu_params( | ||
| 306 | + cfg.npu_params or {}, | ||
| 307 | + enable=False, | ||
| 308 | + ) | ||
| 309 | + new_configs.append(cfg) | ||
| 310 | + cfg_with_npu = FlexAttentionConfig( | ||
| 311 | + block_m=cfg.block_m, | ||
| 312 | + block_n=cfg.block_n, | ||
| 313 | + num_warps=cfg.num_warps, | ||
| 314 | + num_stages=cfg.num_stages, | ||
| 315 | + npu_params=npu_params.copy(), | ||
| 316 | + ) | ||
| 317 | + new_configs.append(cfg_with_npu) | ||
| 318 | + self.configs = new_configs # Replace instead of append | ||
| 319 | + | ||
| 320 | + def _limit_config_count(self): | ||
| 321 | + """ | ||
| 322 | + Limit config count to avoid excessive autotuning time. | ||
| 323 | + | ||
| 324 | + Strategy: Select configs with diverse BLOCK_M/BLOCK_N ratios | ||
| 325 | + """ | ||
| 326 | + if len(self.configs) <= self.MAX_CONFIGS: | ||
| 327 | + return | ||
| 328 | + | ||
| 329 | + ratio_groups: dict[float, list[FlexAttentionConfig]] = {} | ||
| 330 | + for cfg in self.configs: | ||
| 331 | + ratio = cfg.block_m / cfg.block_n | ||
| 332 | + if ratio not in ratio_groups: | ||
| 333 | + ratio_groups[ratio] = [] | ||
| 334 | + ratio_groups[ratio].append(cfg) | ||
| 335 | + | ||
| 336 | + selected = [] | ||
| 337 | + per_group = max(1, self.MAX_CONFIGS // len(ratio_groups)) | ||
| 338 | + for group in ratio_groups.values(): | ||
| 339 | + selected.extend(group[:per_group]) | ||
| 340 | + | ||
| 341 | + self.configs = selected[:self.MAX_CONFIGS] | ||
| 342 | + | ||
| 343 | + def calculate_wave_efficiency(self, block_m: int, block_n: int) -> tuple[int, float]: | ||
| 344 | + """ | ||
| 345 | + Calculate wave efficiency for given block sizes. | ||
| 346 | + | ||
| 347 | + Args: | ||
| 348 | + block_m: BLOCK_M size | ||
| 349 | + block_n: BLOCK_N size | ||
| 350 | + | ||
| 351 | + Returns: | ||
| 352 | + Tuple of (waves, efficiency) | ||
| 353 | + """ | ||
| 354 | + programs_m = (self.seq_len_q + block_m - 1) // block_m | ||
| 355 | + total_programs = programs_m * self.batch_size * self.num_heads | ||
| 356 | + | ||
| 357 | + waves = (total_programs + self.num_cube_core - 1) // self.num_cube_core | ||
| 358 | + | ||
| 359 | + efficiency = total_programs / (waves * self.num_cube_core) if waves > 0 else 0.0 | ||
| 360 | + | ||
| 361 | + return waves, efficiency | ||
| 362 | + | ||
| 363 | +def generate_fwd_configs( | ||
| 364 | + query_shape: tuple, | ||
| 365 | + key_shape: tuple, | ||
| 366 | + sparse_q_block_size: int, | ||
| 367 | + sparse_kv_block_size: int, | ||
| 368 | + dtype: torch.dtype, | ||
| 369 | + num_cube_core: int, | ||
| 370 | +) -> list[dict]: | ||
| 371 | + """ | ||
| 372 | + Convenience function to generate forward configs. | ||
| 373 | + | ||
| 374 | + Args: | ||
| 375 | + query_shape: Shape of query tensor | ||
| 376 | + key_shape: Shape of key tensor | ||
| 377 | + sparse_q_block_size: SPARSE_Q_BLOCK_SIZE | ||
| 378 | + sparse_kv_block_size: SPARSE_KV_BLOCK_SIZE | ||
| 379 | + dtype: Data type | ||
| 380 | + num_cube_core: Number of AICore | ||
| 381 | + | ||
| 382 | + Returns: | ||
| 383 | + List of config dictionaries | ||
| 384 | + """ | ||
| 385 | + generator = FlexAttentionConfigGenerator( | ||
| 386 | + query_shape=query_shape, | ||
| 387 | + key_shape=key_shape, | ||
| 388 | + sparse_q_block_size=sparse_q_block_size, | ||
| 389 | + sparse_kv_block_size=sparse_kv_block_size, | ||
| 390 | + dtype=dtype, | ||
| 391 | + num_cube_core=num_cube_core, | ||
| 392 | + mode=FlexMode.FWD, | ||
| 393 | + ) | ||
| 394 | + return generator.generate_configs() | ||
| 395 | + | ||
| 396 | + | ||
| 397 | +def generate_bwd_configs( | ||
| 398 | + query_shape: tuple, | ||
| 399 | + key_shape: tuple, | ||
| 400 | + sparse_q_block_size: int, | ||
| 401 | + sparse_kv_block_size: int, | ||
| 402 | + dtype: torch.dtype, | ||
| 403 | + num_cube_core: int, | ||
| 404 | +) -> list[dict]: | ||
| 405 | + """ | ||
| 406 | + Convenience function to generate backward configs. | ||
| 407 | + | ||
| 408 | + Args: | ||
| 409 | + query_shape: Shape of query tensor | ||
| 410 | + key_shape: Shape of key tensor | ||
| 411 | + sparse_q_block_size: SPARSE_Q_BLOCK_SIZE | ||
| 412 | + sparse_kv_block_size: SPARSE_KV_BLOCK_SIZE | ||
| 413 | + dtype: Data type | ||
| 414 | + num_cube_core: Number of AICore | ||
| 415 | + | ||
| 416 | + Returns: | ||
| 417 | + List of config dictionaries | ||
| 418 | + """ | ||
| 419 | + generator = FlexAttentionConfigGenerator( | ||
| 420 | + query_shape=query_shape, | ||
| 421 | + key_shape=key_shape, | ||
| 422 | + sparse_q_block_size=sparse_q_block_size, | ||
| 423 | + sparse_kv_block_size=sparse_kv_block_size, | ||
| 424 | + dtype=dtype, | ||
| 425 | + num_cube_core=num_cube_core, | ||
| 426 | + mode=FlexMode.BWD, | ||
| 427 | + ) | ||
| 428 | + return generator.generate_configs() | ||
| 429 | + | ||
| 430 | + | ||
| 431 | +def prefer_max_tiling_without_benchmark() -> bool: | ||
| 432 | + return ( | ||
| 433 | + npu_config.flex_attention.use_config_generator | ||
| 434 | + and not getattr(inductor_config, "max_autotune", False) | ||
| 435 | + and not getattr(inductor_config, "max_autotune_gemm", False) | ||
| 436 | + and not getattr(npu_config, "aggresive_autotune", False) | ||
| 437 | + ) | ||
| 438 | + | ||
| 439 | + | ||
| 440 | +def _sort_fwd_candidate_configs_for_nobench(configs: list[dict]) -> list[dict]: | ||
| 441 | + return sorted( | ||
| 442 | + configs, | ||
| 443 | + key=lambda cfg: ( | ||
| 444 | + int(cfg.get("BLOCK_M", 0)) * int(cfg.get("BLOCK_N", 0)), | ||
| 445 | + int(cfg.get("BLOCK_M", 0)), | ||
| 446 | + int(cfg.get("BLOCK_N", 0)), | ||
| 447 | + ), | ||
| 448 | + reverse=True, | ||
| 449 | + ) | ||
| 450 | + | ||
| 451 | + | ||
| 452 | +_FWD_MASK_IN_TILING_ORDER = ( | ||
| 453 | + (128, 128), | ||
| 454 | + (128, 64), | ||
| 455 | + (64, 128), | ||
| 456 | + (64, 64), | ||
| 457 | + (32, 32), | ||
| 458 | +) | ||
| 459 | + | ||
| 460 | + | ||
| 461 | +def _build_fwd_mask_in_candidate_configs( | ||
| 462 | + configs: list[dict], | ||
| 463 | + *, | ||
| 464 | + sparse_q_block_size: int, | ||
| 465 | + sparse_kv_block_size: int, | ||
| 466 | +) -> list[dict]: | ||
| 467 | + template = configs[0].copy() if configs else { | ||
| 468 | + "num_warps": 4, | ||
| 469 | + "num_stages": 1, | ||
| 470 | + } | ||
| 471 | + ordered_configs = [] | ||
| 472 | + | ||
| 473 | + for block_m, block_n in _FWD_MASK_IN_TILING_ORDER: | ||
| 474 | + if ( | ||
| 475 | + block_m > sparse_q_block_size | ||
| 476 | + or block_n > sparse_kv_block_size | ||
| 477 | + or sparse_q_block_size % block_m != 0 | ||
| 478 | + or sparse_kv_block_size % block_n != 0 | ||
| 479 | + ): | ||
| 480 | + continue | ||
| 481 | + cfg = template.copy() | ||
| 482 | + cfg.update( | ||
| 483 | + { | ||
| 484 | + "BLOCK_M": block_m, | ||
| 485 | + "BLOCK_N": block_n, | ||
| 486 | + "num_warps": cfg.get("num_warps", 4), | ||
| 487 | + "num_stages": 1, | ||
| 488 | + } | ||
| 489 | + ) | ||
| 490 | + ordered_configs.append(cfg) | ||
| 491 | + | ||
| 492 | + if ordered_configs: | ||
| 493 | + return ordered_configs | ||
| 494 | + | ||
| 495 | + return [ | ||
| 496 | + cfg | ||
| 497 | + for cfg in configs | ||
| 498 | + if ( | ||
| 499 | + sparse_q_block_size % int(cfg["BLOCK_M"]) == 0 | ||
| 500 | + and sparse_kv_block_size % int(cfg["BLOCK_N"]) == 0 | ||
| 501 | + ) | ||
| 502 | + ] | ||
| 503 | + | ||
| 504 | + | ||
| 505 | +def _sort_sparse_mask_candidate_configs_for_nobench( | ||
| 506 | + configs: list[dict[str, int]], | ||
| 507 | +) -> list[dict[str, int]]: | ||
| 508 | + return sorted( | ||
| 509 | + configs, | ||
| 510 | + key=lambda cfg: ( | ||
| 511 | + int(cfg["MASK_BLOCK_M"]) * int(cfg["MASK_BLOCK_N"]), | ||
| 512 | + int(cfg["MASK_BLOCK_M"]), | ||
| 513 | + int(cfg["MASK_BLOCK_N"]), | ||
| 514 | + ), | ||
| 515 | + reverse=True, | ||
| 516 | + ) | ||
| 517 | + | ||
| 518 | + | ||
| 519 | +def _get_default_fwd_config(dtype: torch.dtype, head_dim: int) -> dict: | ||
| 520 | + head_dim = int(head_dim) | ||
| 521 | + config = { | ||
| 522 | + "num_warps": 4, | ||
| 523 | + "num_stages": 3, | ||
| 524 | + } | ||
| 525 | + | ||
| 526 | + if head_dim <= 256: | ||
| 527 | + config["BLOCK_M"] = 64 | ||
| 528 | + config["BLOCK_N"] = 64 | ||
| 529 | + elif dtype == torch.float32: | ||
| 530 | + config["BLOCK_M"] = 32 | ||
| 531 | + config["BLOCK_N"] = 16 | ||
| 532 | + else: | ||
| 533 | + config["BLOCK_M"] = 32 | ||
| 534 | + config["BLOCK_N"] = 32 | ||
| 535 | + | ||
| 536 | + return config | ||
| 537 | + | ||
| 538 | + | ||
| 539 | +def _tune_npu_params(configs: list[dict]) -> list[dict]: | ||
| 540 | + enable = npu_config.flex_attention.enable_npu_optimization | ||
| 541 | + if enable: | ||
| 542 | + npu_params = npu_config.flex_attention.get_npu_compile_hint_params() | ||
| 543 | + npu_config.log.info( | ||
| 544 | + "[flex_attention] NPU compile hint enabled with parameters: %s", | ||
| 545 | + npu_params, | ||
| 546 | + ) | ||
| 547 | + log.debug("npu_params: %s", npu_params) | ||
| 548 | + | ||
| 549 | + return [ | ||
| 550 | + npu_config.apply_flex_attention_npu_params(config, enable=enable) | ||
| 551 | + for config in configs | ||
| 552 | + ] | ||
| 553 | + | ||
| 554 | + | ||
| 555 | +def _build_single_fwd_config( | ||
| 556 | + dtype: torch.dtype, | ||
| 557 | + head_dim: int, | ||
| 558 | + sparse_q_block_size: int, | ||
| 559 | + sparse_kv_block_size: int, | ||
| 560 | +) -> list[dict]: | ||
| 561 | + config = _get_default_fwd_config(dtype, head_dim) | ||
| 562 | + if sparse_q_block_size % config["BLOCK_M"] != 0: | ||
| 563 | + config["BLOCK_M"] = int(sparse_q_block_size) | ||
| 564 | + if sparse_kv_block_size % config["BLOCK_N"] != 0: | ||
| 565 | + config["BLOCK_N"] = int(sparse_kv_block_size) | ||
| 566 | + return _tune_npu_params([config]) | ||
| 567 | + | ||
| 568 | + | ||
| 569 | +def get_bwd_dq_compile_options() -> dict: | ||
| 570 | + return npu_config.flex_attention.get_bwd_dq_compile_options() | ||
| 571 | + | ||
| 572 | + | ||
| 573 | +def get_bwd_dkdv_compile_options() -> dict: | ||
| 574 | + return npu_config.flex_attention.get_bwd_dkdv_compile_options() | ||
| 575 | + | ||
| 576 | + | ||
| 577 | +def generate_fwd_candidate_configs( | ||
| 578 | + query_shape: tuple, | ||
| 579 | + key_shape: tuple, | ||
| 580 | + dtype: torch.dtype, | ||
| 581 | + sparse_q_block_size: int, | ||
| 582 | + sparse_kv_block_size: int, | ||
| 583 | + num_cube_core: int, | ||
| 584 | + head_dim: Optional[int] = None, | ||
| 585 | + mask_out: bool = True, | ||
| 586 | +) -> list[dict]: | ||
| 587 | + """ | ||
| 588 | + Generate candidate configs for forward flex attention. | ||
| 589 | + | ||
| 590 | + This wrapper owns the generator/fallback policy so the lowering file only | ||
| 591 | + needs to pass ordinary Python values extracted from IR nodes. | ||
| 592 | + """ | ||
| 593 | + if npu_config.flex_attention.use_config_generator: | ||
| 594 | + configs = generate_fwd_configs( | ||
| 595 | + query_shape=query_shape, | ||
| 596 | + key_shape=key_shape, | ||
| 597 | + sparse_q_block_size=sparse_q_block_size, | ||
| 598 | + sparse_kv_block_size=sparse_kv_block_size, | ||
| 599 | + dtype=dtype, | ||
| 600 | + num_cube_core=num_cube_core, | ||
| 601 | + ) | ||
| 602 | + if prefer_max_tiling_without_benchmark(): | ||
| 603 | + configs = _sort_fwd_candidate_configs_for_nobench(configs) | ||
| 604 | + if not mask_out: | ||
| 605 | + configs = _build_fwd_mask_in_candidate_configs( | ||
| 606 | + configs, | ||
| 607 | + sparse_q_block_size=sparse_q_block_size, | ||
| 608 | + sparse_kv_block_size=sparse_kv_block_size, | ||
| 609 | + ) | ||
| 610 | + return configs | ||
| 611 | + | ||
| 612 | + if head_dim is None: | ||
| 613 | + head_dim = int(query_shape[-1]) | ||
| 614 | + configs = _build_single_fwd_config( | ||
| 615 | + dtype=dtype, | ||
| 616 | + head_dim=head_dim, | ||
| 617 | + sparse_q_block_size=sparse_q_block_size, | ||
| 618 | + sparse_kv_block_size=sparse_kv_block_size, | ||
| 619 | + ) | ||
| 620 | + if not mask_out: | ||
| 621 | + configs = _build_fwd_mask_in_candidate_configs( | ||
| 622 | + configs, | ||
| 623 | + sparse_q_block_size=sparse_q_block_size, | ||
| 624 | + sparse_kv_block_size=sparse_kv_block_size, | ||
| 625 | + ) | ||
| 626 | + return configs | ||
| 627 | + | ||
| 628 | + | ||
| 629 | +def _flex_attention_sparse_mask_block_candidates(sparse_block_size: int) -> list[int]: | ||
| 630 | + sparse_block_size = int(sparse_block_size) | ||
| 631 | + if sparse_block_size <= 0: | ||
| 632 | + raise ValueError(f"sparse block size must be positive, got {sparse_block_size}") | ||
| 633 | + | ||
| 634 | + min_mask_block = min(16, sparse_block_size) | ||
| 635 | + candidates = [] | ||
| 636 | + mask_block = sparse_block_size | ||
| 637 | + while mask_block >= min_mask_block: | ||
| 638 | + candidates.append(mask_block) | ||
| 639 | + mask_block //= 2 | ||
| 640 | + | ||
| 641 | + for fallback_mask_block in (64, 32, 16): | ||
| 642 | + if fallback_mask_block <= sparse_block_size: | ||
| 643 | + candidates.append(fallback_mask_block) | ||
| 644 | + | ||
| 645 | + unique_candidates = [] | ||
| 646 | + seen = set() | ||
| 647 | + for mask_block in sorted(candidates, reverse=True): | ||
| 648 | + if mask_block in seen: | ||
| 649 | + continue | ||
| 650 | + if sparse_block_size % mask_block != 0: | ||
| 651 | + continue | ||
| 652 | + seen.add(mask_block) | ||
| 653 | + unique_candidates.append(mask_block) | ||
| 654 | + | ||
| 655 | + return unique_candidates | ||
| 656 | + | ||
| 657 | + | ||
| 658 | +def _flex_attention_sparse_mask_tiling_configs( | ||
| 659 | + sparse_q_block_size: int, | ||
| 660 | + sparse_kv_block_size: int, | ||
| 661 | +) -> list[dict[str, int]]: | ||
| 662 | + sparse_q_block_size = int(sparse_q_block_size) | ||
| 663 | + sparse_kv_block_size = int(sparse_kv_block_size) | ||
| 664 | + if sparse_q_block_size <= 0: | ||
| 665 | + raise ValueError( | ||
| 666 | + f"SPARSE_Q_BLOCK_SIZE must be positive, got {sparse_q_block_size}" | ||
| 667 | + ) | ||
| 668 | + if sparse_kv_block_size <= 0: | ||
| 669 | + raise ValueError( | ||
| 670 | + f"SPARSE_KV_BLOCK_SIZE must be positive, got {sparse_kv_block_size}" | ||
| 671 | + ) | ||
| 672 | + | ||
| 673 | + mask_block_m_candidates = _flex_attention_sparse_mask_block_candidates( | ||
| 674 | + sparse_q_block_size | ||
| 675 | + ) | ||
| 676 | + mask_block_n_candidates = _flex_attention_sparse_mask_block_candidates( | ||
| 677 | + sparse_kv_block_size | ||
| 678 | + ) | ||
| 679 | + | ||
| 680 | + configs = [] | ||
| 681 | + seen = set() | ||
| 682 | + candidate_pairs = ( | ||
| 683 | + (mask_block_m, mask_block_n) | ||
| 684 | + for mask_block_m in mask_block_m_candidates | ||
| 685 | + for mask_block_n in mask_block_n_candidates | ||
| 686 | + ) | ||
| 687 | + | ||
| 688 | + for mask_block_m, mask_block_n in candidate_pairs: | ||
| 689 | + if (mask_block_m, mask_block_n) in seen: | ||
| 690 | + continue | ||
| 691 | + seen.add((mask_block_m, mask_block_n)) | ||
| 692 | + configs.append( | ||
| 693 | + { | ||
| 694 | + "MASK_BLOCK_M": mask_block_m, | ||
| 695 | + "MASK_BLOCK_N": mask_block_n, | ||
| 696 | + "NUM_Q_SUB_BLOCKS": sparse_q_block_size // mask_block_m, | ||
| 697 | + "NUM_KV_SUB_BLOCKS": sparse_kv_block_size // mask_block_n, | ||
| 698 | + "num_warps": 4, | ||
| 699 | + "num_stages": 1, | ||
| 700 | + } | ||
| 701 | + ) | ||
| 702 | + | ||
| 703 | + return configs | ||
| 704 | + | ||
| 705 | + | ||
| 706 | +def _get_default_sparse_mask_tiling_config( | ||
| 707 | + sparse_q_block_size: int, | ||
| 708 | + sparse_kv_block_size: int, | ||
| 709 | +) -> dict[str, int]: | ||
| 710 | + sparse_q_block_size = int(sparse_q_block_size) | ||
| 711 | + sparse_kv_block_size = int(sparse_kv_block_size) | ||
| 712 | + return { | ||
| 713 | + "MASK_BLOCK_M": sparse_q_block_size, | ||
| 714 | + "MASK_BLOCK_N": sparse_kv_block_size, | ||
| 715 | + "NUM_Q_SUB_BLOCKS": 1, | ||
| 716 | + "NUM_KV_SUB_BLOCKS": 1, | ||
| 717 | + "num_warps": 4, | ||
| 718 | + "num_stages": 1, | ||
| 719 | + } | ||
| 720 | + | ||
| 721 | + | ||
| 722 | +def build_sparse_mask_candidate_configs( | ||
| 723 | + sparse_q_block_size: int, | ||
| 724 | + sparse_kv_block_size: int, | ||
| 725 | +) -> list[dict[str, int]]: | ||
| 726 | + """Generate sparse mask materialize kernel tiling candidates.""" | ||
| 727 | + if npu_config.flex_attention.use_config_generator: | ||
| 728 | + configs = _flex_attention_sparse_mask_tiling_configs( | ||
| 729 | + sparse_q_block_size, | ||
| 730 | + sparse_kv_block_size, | ||
| 731 | + ) | ||
| 732 | + if prefer_max_tiling_without_benchmark(): | ||
| 733 | + configs = _sort_sparse_mask_candidate_configs_for_nobench(configs) | ||
| 734 | + return configs | ||
| 735 | + return [ | ||
| 736 | + _get_default_sparse_mask_tiling_config( | ||
| 737 | + sparse_q_block_size, | ||
| 738 | + sparse_kv_block_size, | ||
| 739 | + ) | ||
| 740 | + ] | ||
| 741 | + | ||
| 742 | + | ||
| 743 | +def split_attention_block_n_candidates( | ||
| 744 | + base_block_n: int, | ||
| 745 | + min_block_n: int = 64, | ||
| 746 | +) -> list[int]: | ||
| 747 | + base_block_n = int(base_block_n) | ||
| 748 | + min_block_n = int(min_block_n) | ||
| 749 | + if base_block_n <= 0: | ||
| 750 | + raise ValueError(f"base_block_n must be positive, got {base_block_n}") | ||
| 751 | + if min_block_n <= 0: | ||
| 752 | + raise ValueError(f"min_block_n must be positive, got {min_block_n}") | ||
| 753 | + | ||
| 754 | + candidates: list[int] = [] | ||
| 755 | + current = base_block_n | ||
| 756 | + while current >= min_block_n: | ||
| 757 | + if base_block_n % current == 0: | ||
| 758 | + candidates.append(current) | ||
| 759 | + current //= 2 | ||
| 760 | + | ||
| 761 | + if not candidates: | ||
| 762 | + candidates.append(base_block_n) | ||
| 763 | + return candidates | ||
| 764 | + | ||
| 765 | + | ||
| 766 | +def _sparse_mask_attention_tile_mix_loop(block_n: int) -> int: | ||
| 767 | + block_n = int(block_n) | ||
| 768 | + if block_n >= 512: | ||
| 769 | + return 4 | ||
| 770 | + if block_n >= 256: | ||
| 771 | + return 2 | ||
| 772 | + if block_n >= 128: | ||
| 773 | + return 1 | ||
| 774 | + return 0 | ||
| 775 | + | ||
| 776 | + | ||
| 777 | +def _sparse_mask_attention_cvpipeline_options( | ||
| 778 | + block_n: int, | ||
| 779 | + *, | ||
| 780 | + enabled: bool, | ||
| 781 | + enable_compile_hint: bool = False, | ||
| 782 | +) -> dict[str, Union[int, bool, str]]: | ||
| 783 | + tile_mix_loop = _sparse_mask_attention_tile_mix_loop(block_n) if enabled else 0 | ||
| 784 | + return npu_config.flex_attention.get_sparse_mask_cvpipeline_compile_options( | ||
| 785 | + enabled=enabled, | ||
| 786 | + tile_mix_loop=tile_mix_loop, | ||
| 787 | + enable_compile_hint=enable_compile_hint, | ||
| 788 | + ) | ||
| 789 | + | ||
| 790 | + | ||
| 791 | +def sparse_mask_attention_cvpipeline_config_variants( | ||
| 792 | + base_options: dict, | ||
| 793 | + *, | ||
| 794 | + block_n: int, | ||
| 795 | + enable_compile_hint: bool = False, | ||
| 796 | +) -> list[dict]: | ||
| 797 | + variants = [] | ||
| 798 | + for enabled in (True, False): | ||
| 799 | + variant = base_options.copy() | ||
| 800 | + variant.update( | ||
| 801 | + _sparse_mask_attention_cvpipeline_options( | ||
| 802 | + block_n, | ||
| 803 | + enabled=enabled, | ||
| 804 | + enable_compile_hint=enable_compile_hint, | ||
| 805 | + ) | ||
| 806 | + ) | ||
| 807 | + variants.append(variant) | ||
| 808 | + return variants | ||
| 809 | + | ||
| 810 | + | ||
| 811 | +def is_bwd_config_compatible( | ||
| 812 | + cfg: dict, | ||
| 813 | + sparse_q_block_size: int, | ||
| 814 | + sparse_kv_block_size: int, | ||
| 815 | +) -> bool: | ||
| 816 | + block_m1 = cfg["BLOCK_M1"] | ||
| 817 | + block_n1 = cfg["BLOCK_N1"] | ||
| 818 | + block_m2 = cfg["BLOCK_M2"] | ||
| 819 | + block_n2 = cfg["BLOCK_N2"] | ||
| 820 | + return ( | ||
| 821 | + sparse_q_block_size % block_m1 == 0 | ||
| 822 | + and sparse_kv_block_size % block_n1 == 0 | ||
| 823 | + and sparse_q_block_size % block_m2 == 0 | ||
| 824 | + and sparse_kv_block_size % block_n2 == 0 | ||
| 825 | + ) | ||
| 826 | + | ||
| 827 | + | ||
| 828 | +def _convert_bwd_config_to_fused_mask_out_config(cfg: dict) -> dict: | ||
| 829 | + converted_cfg = { | ||
| 830 | + "BLOCK_M1": cfg["BLOCK_M"], | ||
| 831 | + "BLOCK_N1": cfg["BLOCK_N"], | ||
| 832 | + "BLOCK_M2": cfg["BLOCK_N"], | ||
| 833 | + "BLOCK_N2": cfg["BLOCK_M"], | ||
| 834 | + "num_warps": cfg["num_warps"], | ||
| 835 | + "num_stages": cfg["num_stages"], | ||
| 836 | + } | ||
| 837 | + for key, value in cfg.items(): | ||
| 838 | + if key not in ("BLOCK_M", "BLOCK_N", "num_warps", "num_stages"): | ||
| 839 | + converted_cfg[key] = value | ||
| 840 | + return converted_cfg | ||
| 841 | + | ||
| 842 | + | ||
| 843 | +def _start_bwd_mask_out_from_128x128_configs( | ||
| 844 | + configs: list[dict], | ||
| 845 | + *, | ||
| 846 | + sparse_q_block_size: int, | ||
| 847 | + sparse_kv_block_size: int, | ||
| 848 | +) -> list[dict]: | ||
| 849 | + sparse_q_block_size = int(sparse_q_block_size) | ||
| 850 | + sparse_kv_block_size = int(sparse_kv_block_size) | ||
| 851 | + max_block = min(128, sparse_q_block_size, sparse_kv_block_size) | ||
| 852 | + | ||
| 853 | + preferred_blocks = [] | ||
| 854 | + block = max_block | ||
| 855 | + min_block = min(16, max_block) | ||
| 856 | + while block >= min_block: | ||
| 857 | + if sparse_q_block_size % block == 0 and sparse_kv_block_size % block == 0: | ||
| 858 | + preferred_blocks.append(block) | ||
| 859 | + block //= 2 | ||
| 860 | + if not preferred_blocks: | ||
| 861 | + preferred_blocks.append(max_block) | ||
| 862 | + | ||
| 863 | + template = configs[0].copy() if configs else {"num_warps": 4, "num_stages": 1} | ||
| 864 | + ordered_configs = [] | ||
| 865 | + seen_configs = set() | ||
| 866 | + tiling_keys = ("BLOCK_M1", "BLOCK_N1", "BLOCK_M2", "BLOCK_N2") | ||
| 867 | + | ||
| 868 | + for block in preferred_blocks: | ||
| 869 | + cfg = template.copy() | ||
| 870 | + cfg.update( | ||
| 871 | + { | ||
| 872 | + "BLOCK_M1": block, | ||
| 873 | + "BLOCK_N1": block, | ||
| 874 | + "BLOCK_M2": block, | ||
| 875 | + "BLOCK_N2": block, | ||
| 876 | + "num_warps": cfg.get("num_warps", 4), | ||
| 877 | + "num_stages": cfg.get("num_stages", 1), | ||
| 878 | + } | ||
| 879 | + ) | ||
| 880 | + config_key = tuple(cfg.get(key) for key in tiling_keys) | ||
| 881 | + if config_key in seen_configs: | ||
| 882 | + continue | ||
| 883 | + seen_configs.add(config_key) | ||
| 884 | + ordered_configs.append(cfg) | ||
| 885 | + | ||
| 886 | + for cfg in configs: | ||
| 887 | + config_key = tuple(cfg.get(key) for key in tiling_keys) | ||
| 888 | + if config_key in seen_configs: | ||
| 889 | + continue | ||
| 890 | + seen_configs.add(config_key) | ||
| 891 | + ordered_configs.append(cfg) | ||
| 892 | + return ordered_configs | ||
| 893 | + | ||
| 894 | + | ||
| 895 | +def generate_bwd_fused_mask_out_candidate_configs( | ||
| 896 | + query_shape: tuple, | ||
| 897 | + key_shape: tuple, | ||
| 898 | + sparse_q_block_size: int, | ||
| 899 | + sparse_kv_block_size: int, | ||
| 900 | + dtype: torch.dtype, | ||
| 901 | + num_cube_core: int, | ||
| 902 | +) -> list[dict]: | ||
| 903 | + """ | ||
| 904 | + Generate candidate configs for the fused compact sparse mask-out backward path. | ||
| 905 | + | ||
| 906 | + The fused backward kernel uses the split backward tiling names but runs as a | ||
| 907 | + single compact sparse mask-out template. Keep 128x128 square tiling first so | ||
| 908 | + the generated output_code remains aligned with the verified path. | ||
| 909 | + """ | ||
| 910 | + base_configs = generate_bwd_configs( | ||
| 911 | + query_shape=query_shape, | ||
| 912 | + key_shape=key_shape, | ||
| 913 | + sparse_q_block_size=sparse_q_block_size, | ||
| 914 | + sparse_kv_block_size=sparse_kv_block_size, | ||
| 915 | + dtype=dtype, | ||
| 916 | + num_cube_core=num_cube_core, | ||
| 917 | + ) | ||
| 918 | + configs = [ | ||
| 919 | + _convert_bwd_config_to_fused_mask_out_config(cfg) | ||
| 920 | + for cfg in base_configs | ||
| 921 | + ] | ||
| 922 | + return _start_bwd_mask_out_from_128x128_configs( | ||
| 923 | + configs, | ||
| 924 | + sparse_q_block_size=sparse_q_block_size, | ||
| 925 | + sparse_kv_block_size=sparse_kv_block_size, | ||
| 926 | + ) | ||
| 927 | + | ||
| 928 | + | ||
| 929 | +def generate_bwd_split_mask_out_candidate_configs( | ||
| 930 | + query_shape: tuple, | ||
| 931 | + key_shape: tuple, | ||
| 932 | + sparse_q_block_size: int, | ||
| 933 | + sparse_kv_block_size: int, | ||
| 934 | + dtype: torch.dtype, | ||
| 935 | + num_cube_core: int, | ||
| 936 | +) -> list[dict]: | ||
| 937 | + """Generate candidate configs for split DQ and DKDV backward mask-out kernels.""" | ||
| 938 | + return generate_bwd_fused_mask_out_candidate_configs( | ||
| 939 | + query_shape=query_shape, | ||
| 940 | + key_shape=key_shape, | ||
| 941 | + sparse_q_block_size=sparse_q_block_size, | ||
| 942 | + sparse_kv_block_size=sparse_kv_block_size, | ||
| 943 | + dtype=dtype, | ||
| 944 | + num_cube_core=num_cube_core, | ||
| 945 | + ) | ||
| 946 | + | ||
| 947 | + | ||
| 948 | +def validate_benchmark_config() -> None: | ||
| 949 | + """ | ||
| 950 | + Validate benchmark configuration before autotuning. | ||
| 951 | + | ||
| 952 | + This function checks that required configurations are enabled for | ||
| 953 | + NPU optimized benchmark. | ||
| 954 | + | ||
| 955 | + Note: This function now only warns instead of raising errors to avoid | ||
| 956 | + blocking execution. The actual benchmark will use fallback methods if | ||
| 957 | + configurations are not optimal. | ||
| 958 | + """ | ||
| 959 | + aggresive_autotune = getattr(npu_config, 'aggresive_autotune', False) | ||
| 960 | + max_autotune = getattr(inductor_config, 'max_autotune', False) | ||
| 961 | + | ||
| 962 | + if not aggresive_autotune: | ||
| 963 | + log.warning( | ||
| 964 | + "aggresive_autotune is False. NPU optimized benchmark is disabled. " | ||
| 965 | + "For optimal performance, set INDUCTOR_ASCEND_AGGRESSIVE_AUTOTUNE=1 environment variable. " | ||
| 966 | + "Continuing with fallback benchmark method." | ||
| 967 | + ) | ||
| 968 | + | ||
| 969 | + if not max_autotune: | ||
| 970 | + log.warning( | ||
| 971 | + "max_autotune is False, only default config will be used. " | ||
| 972 | + "Set TORCHINDUCTOR_MAX_AUTOTUNE=1 for multi-config autotuning." | ||
| 973 | + ) | ||
| @@ -0,0 +1,1051 @@ | |||
| 1 | +from __future__ import annotations | ||
| 2 | + | ||
| 3 | +import logging | ||
| 4 | +from typing import Any | ||
| 5 | + | ||
| 6 | +import torch | ||
| 7 | + | ||
| 8 | +from torch_npu._inductor import config as npu_config | ||
| 9 | + | ||
| 10 | +log = npu_config.log | ||
| 11 | + | ||
| 12 | + | ||
| 13 | +def _metadata_auto_infer_enabled() -> bool: | ||
| 14 | + """Return whether metadata auto inference is enabled by config.""" | ||
| 15 | + flex_attention_config = getattr(npu_config, "flex_attention", None) | ||
| 16 | + if flex_attention_config is None: | ||
| 17 | + return True | ||
| 18 | + return getattr(flex_attention_config, "metadata_auto_infer", True) | ||
| 19 | + | ||
| 20 | + | ||
| 21 | +def _try_unwrap_tensor(value: Any) -> torch.Tensor | None: | ||
| 22 | + """Extract a torch.Tensor from wrapper objects that expose a data attribute.""" | ||
| 23 | + if isinstance(value, torch.Tensor): | ||
| 24 | + return value | ||
| 25 | + | ||
| 26 | + data_value = getattr(value, "data", None) | ||
| 27 | + if data_value is value or data_value is None: | ||
| 28 | + return None | ||
| 29 | + | ||
| 30 | + return _try_unwrap_tensor(data_value) | ||
| 31 | + | ||
| 32 | + | ||
| 33 | +_SPARSE_MASK_COMPACT_OPTION_KEYS = ( | ||
| 34 | + "SPARSE_MASK_MAX_NORMAL_BLOCKS", | ||
| 35 | + "SPARSE_MASK_HEAD_SHARED", | ||
| 36 | + "SPARSE_MASK_HQ", | ||
| 37 | + "HAS_FULL_BLOCKS", | ||
| 38 | +) | ||
| 39 | +_BLOCK_SPARSE_SAFETY_OPTION_KEYS = ( | ||
| 40 | + "NPU_ROWS_GUARANTEED_SAFE", | ||
| 41 | + "NPU_BLOCKS_ARE_CONTIGUOUS", | ||
| 42 | +) | ||
| 43 | + | ||
| 44 | + | ||
| 45 | +def _to_cpu_int_tensor(value: Any) -> torch.Tensor | None: | ||
| 46 | + """Best-effort conversion of a tensor-like value to a CPU int64 tensor.""" | ||
| 47 | + tensor = _try_unwrap_tensor(value) | ||
| 48 | + if tensor is None: | ||
| 49 | + return None | ||
| 50 | + try: | ||
| 51 | + return tensor.detach().to("cpu", dtype=torch.int64) | ||
| 52 | + except Exception: | ||
| 53 | + return None | ||
| 54 | + | ||
| 55 | + | ||
| 56 | +def _heads_share_used_block_entries( | ||
| 57 | + num_blocks: torch.Tensor | None, indices: torch.Tensor | None | ||
| 58 | +) -> bool: | ||
| 59 | + """ | ||
| 60 | + Return True when all heads have identical used sparse-block entries. | ||
| 61 | + | ||
| 62 | + Only the valid prefix of each row is compared because entries after | ||
| 63 | + num_blocks[b, h, q] are undefined padding in BlockMask. | ||
| 64 | + """ | ||
| 65 | + if num_blocks is None or indices is None: | ||
| 66 | + return False | ||
| 67 | + if num_blocks.ndim < 3 or indices.ndim < 4: | ||
| 68 | + return True | ||
| 69 | + | ||
| 70 | + batch = int(num_blocks.shape[0]) | ||
| 71 | + heads = int(num_blocks.shape[1]) | ||
| 72 | + rows = int(num_blocks.shape[2]) | ||
| 73 | + capacity = int(indices.shape[-1]) | ||
| 74 | + if heads <= 1: | ||
| 75 | + return True | ||
| 76 | + | ||
| 77 | + for b_idx in range(batch): | ||
| 78 | + for q_idx in range(rows): | ||
| 79 | + ref_count = int(num_blocks[b_idx, 0, q_idx].item()) | ||
| 80 | + ref_count = max(0, min(ref_count, capacity)) | ||
| 81 | + ref_indices = indices[b_idx, 0, q_idx, :ref_count] | ||
| 82 | + for h_idx in range(1, heads): | ||
| 83 | + cur_count = int(num_blocks[b_idx, h_idx, q_idx].item()) | ||
| 84 | + cur_count = max(0, min(cur_count, capacity)) | ||
| 85 | + if cur_count != ref_count: | ||
| 86 | + return False | ||
| 87 | + cur_indices = indices[b_idx, h_idx, q_idx, :cur_count] | ||
| 88 | + if not torch.equal(cur_indices, ref_indices): | ||
| 89 | + return False | ||
| 90 | + return True | ||
| 91 | + | ||
| 92 | + | ||
| 93 | +def _infer_sparse_mask_compact_options(block_mask: Any) -> dict[str, Any]: | ||
| 94 | + """ | ||
| 95 | + Infer compact sparse-mask materialization options from eager BlockMask metadata. | ||
| 96 | + | ||
| 97 | + The options specialize the temporary mask buffer only. KV traversal still uses | ||
| 98 | + the original BlockMask tensors, so failures to inspect simply return no options | ||
| 99 | + and leave the existing uncompressed shape in place. | ||
| 100 | + """ | ||
| 101 | + if block_mask is None: | ||
| 102 | + return {} | ||
| 103 | + | ||
| 104 | + kv_num_blocks = _to_cpu_int_tensor(getattr(block_mask, "kv_num_blocks", None)) | ||
| 105 | + kv_indices = _to_cpu_int_tensor(getattr(block_mask, "kv_indices", None)) | ||
| 106 | + if kv_num_blocks is None or kv_indices is None: | ||
| 107 | + return {} | ||
| 108 | + if kv_num_blocks.numel() == 0 or kv_indices.ndim < 4: | ||
| 109 | + return {} | ||
| 110 | + | ||
| 111 | + metadata_heads = int(kv_num_blocks.shape[1]) if kv_num_blocks.ndim >= 2 else 1 | ||
| 112 | + capacity = int(kv_indices.shape[-1]) | ||
| 113 | + max_normal_blocks = int(kv_num_blocks.max().item()) | ||
| 114 | + max_normal_blocks = max(1, min(max_normal_blocks, capacity)) | ||
| 115 | + | ||
| 116 | + partial_heads_shared = _heads_share_used_block_entries(kv_num_blocks, kv_indices) | ||
| 117 | + | ||
| 118 | + full_kv_num_blocks = _to_cpu_int_tensor( | ||
| 119 | + getattr(block_mask, "full_kv_num_blocks", None) | ||
| 120 | + ) | ||
| 121 | + full_kv_indices = _to_cpu_int_tensor(getattr(block_mask, "full_kv_indices", None)) | ||
| 122 | + has_full_blocks = bool( | ||
| 123 | + full_kv_num_blocks is not None | ||
| 124 | + and full_kv_num_blocks.numel() > 0 | ||
| 125 | + and full_kv_num_blocks.max().item() > 0 | ||
| 126 | + ) | ||
| 127 | + | ||
| 128 | + if full_kv_num_blocks is None and full_kv_indices is None: | ||
| 129 | + full_heads_shared = True | ||
| 130 | + elif full_kv_num_blocks is None or full_kv_indices is None: | ||
| 131 | + full_heads_shared = False | ||
| 132 | + else: | ||
| 133 | + full_heads_shared = _heads_share_used_block_entries( | ||
| 134 | + full_kv_num_blocks, full_kv_indices | ||
| 135 | + ) | ||
| 136 | + | ||
| 137 | + head_shared = partial_heads_shared and full_heads_shared | ||
| 138 | + options = { | ||
| 139 | + "SPARSE_MASK_MAX_NORMAL_BLOCKS": max_normal_blocks, | ||
| 140 | + "SPARSE_MASK_HEAD_SHARED": bool(head_shared), | ||
| 141 | + "SPARSE_MASK_HQ": 1 if head_shared else metadata_heads, | ||
| 142 | + } | ||
| 143 | + options["HAS_FULL_BLOCKS"] = has_full_blocks | ||
| 144 | + return options | ||
| 145 | + | ||
| 146 | + | ||
| 147 | +def _precomputed_sparse_mask_compact_options(block_mask: Any) -> dict[str, Any]: | ||
| 148 | + """Read compact options cached on a BlockMask by the NPU patch, if present.""" | ||
| 149 | + if block_mask is None: | ||
| 150 | + return {} | ||
| 151 | + options = getattr(block_mask, "_npu_flex_attention_kernel_options", None) | ||
| 152 | + if not isinstance(options, dict): | ||
| 153 | + return {} | ||
| 154 | + return { | ||
| 155 | + key: options[key] | ||
| 156 | + for key in _SPARSE_MASK_COMPACT_OPTION_KEYS | ||
| 157 | + if key in options | ||
| 158 | + } | ||
| 159 | + | ||
| 160 | + | ||
| 161 | +def _precomputed_block_sparse_safety_options(block_mask: Any) -> dict[str, Any]: | ||
| 162 | + """Read exact safety diagnostics cached on a BlockMask by the NPU patch.""" | ||
| 163 | + if block_mask is None: | ||
| 164 | + return {} | ||
| 165 | + options = getattr(block_mask, "_npu_flex_attention_kernel_options", None) | ||
| 166 | + if not isinstance(options, dict): | ||
| 167 | + return {} | ||
| 168 | + return { | ||
| 169 | + key: options[key] | ||
| 170 | + for key in _BLOCK_SPARSE_SAFETY_OPTION_KEYS | ||
| 171 | + if key in options | ||
| 172 | + } | ||
| 173 | + | ||
| 174 | + | ||
| 175 | +def _apply_sparse_mask_compact_options( | ||
| 176 | + kernel_options: dict[str, Any], | ||
| 177 | + block_mask: Any, | ||
| 178 | + context: str, | ||
| 179 | + *, | ||
| 180 | + allow_tensor_analysis: bool, | ||
| 181 | +) -> dict[str, Any]: | ||
| 182 | + """Merge cached or freshly inferred sparse-mask compact options.""" | ||
| 183 | + updated = dict(kernel_options) | ||
| 184 | + compact_options = _precomputed_sparse_mask_compact_options(block_mask) | ||
| 185 | + missing_compact_options = any( | ||
| 186 | + key not in updated for key in _SPARSE_MASK_COMPACT_OPTION_KEYS | ||
| 187 | + ) | ||
| 188 | + if allow_tensor_analysis and missing_compact_options: | ||
| 189 | + compact_options = { | ||
| 190 | + **_infer_sparse_mask_compact_options(block_mask), | ||
| 191 | + **compact_options, | ||
| 192 | + } | ||
| 193 | + | ||
| 194 | + for key, value in compact_options.items(): | ||
| 195 | + updated.setdefault(key, value) | ||
| 196 | + | ||
| 197 | + if compact_options and log.isEnabledFor(logging.INFO): | ||
| 198 | + log.info( | ||
| 199 | + "[flex_attention][%s] sparse_mask_compact_options=%s final_hq=%s final_max_blocks=%s", | ||
| 200 | + context, | ||
| 201 | + compact_options, | ||
| 202 | + updated.get("SPARSE_MASK_HQ", "<unset>"), | ||
| 203 | + updated.get("SPARSE_MASK_MAX_NORMAL_BLOCKS", "<unset>"), | ||
| 204 | + ) | ||
| 205 | + return updated | ||
| 206 | + | ||
| 207 | + | ||
| 208 | +def _apply_precomputed_block_sparse_safety_options( | ||
| 209 | + kernel_options: dict[str, Any], | ||
| 210 | + block_mask: Any, | ||
| 211 | + context: str, | ||
| 212 | +) -> dict[str, Any]: | ||
| 213 | + """Forward cached contiguity diagnostics while keeping row safety conservative.""" | ||
| 214 | + updated = dict(kernel_options) | ||
| 215 | + safety_options = _precomputed_block_sparse_safety_options(block_mask) | ||
| 216 | + if "NPU_BLOCKS_ARE_CONTIGUOUS" in safety_options: | ||
| 217 | + updated.setdefault( | ||
| 218 | + "BLOCKS_ARE_CONTIGUOUS", | ||
| 219 | + bool(safety_options["NPU_BLOCKS_ARE_CONTIGUOUS"]), | ||
| 220 | + ) | ||
| 221 | + if safety_options and log.isEnabledFor(logging.INFO): | ||
| 222 | + log.info( | ||
| 223 | + "[flex_attention][%s] cached_block_sparse_safety_diagnostics=%s " | ||
| 224 | + "forwarded_ROWS_GUARANTEED_SAFE=%s forwarded_BLOCKS_ARE_CONTIGUOUS=%s", | ||
| 225 | + context, | ||
| 226 | + safety_options, | ||
| 227 | + updated.get("ROWS_GUARANTEED_SAFE", "<unset>"), | ||
| 228 | + updated.get("BLOCKS_ARE_CONTIGUOUS", "<unset>"), | ||
| 229 | + ) | ||
| 230 | + return updated | ||
| 231 | + | ||
| 232 | + | ||
| 233 | +def _apply_disabled_metadata_defaults(kernel_options: dict[str, Any]) -> dict[str, Any]: | ||
| 234 | + """Use conservative kernel defaults when metadata auto inference is disabled.""" | ||
| 235 | + updated = dict(kernel_options) | ||
| 236 | + updated.setdefault("ROWS_GUARANTEED_SAFE", False) | ||
| 237 | + updated.setdefault("BLOCKS_ARE_CONTIGUOUS", False) | ||
| 238 | + return updated | ||
| 239 | + | ||
| 240 | + | ||
| 241 | +def _normalize_block_rows(kv_num_blocks: torch.Tensor, kv_indices: torch.Tensor) -> tuple[list[int], list[list[int]]]: | ||
| 242 | + """Convert block-sparse row counts and indices into CPU Python lists.""" | ||
| 243 | + counts = kv_num_blocks.to("cpu", dtype=torch.int64).reshape(-1) | ||
| 244 | + indices = kv_indices.to("cpu", dtype=torch.int64).reshape(-1, kv_indices.shape[-1]) | ||
| 245 | + row_counts = counts.tolist() | ||
| 246 | + row_indices = [indices[i, :count].tolist() for i, count in enumerate(row_counts)] | ||
| 247 | + return row_counts, row_indices | ||
| 248 | + | ||
| 249 | + | ||
| 250 | +def _infer_blocks_are_contiguous_from_tensors(kv_num_blocks: Any, kv_indices: Any) -> bool | str: | ||
| 251 | + counts_tensor = _try_unwrap_tensor(kv_num_blocks) | ||
| 252 | + indices_tensor = _try_unwrap_tensor(kv_indices) | ||
| 253 | + if counts_tensor is None or indices_tensor is None: | ||
| 254 | + return "Unknown" | ||
| 255 | + | ||
| 256 | + _, row_indices = _normalize_block_rows(counts_tensor, indices_tensor) | ||
| 257 | + for values in row_indices: | ||
| 258 | + if len(values) <= 1: | ||
| 259 | + continue | ||
| 260 | + if any((right - left) != 1 for left, right in zip(values, values[1:])): | ||
| 261 | + return False | ||
| 262 | + return True | ||
| 263 | + | ||
| 264 | + | ||
| 265 | +def _layer2_fast_prefilter(block_mask: Any) -> tuple[bool, str]: | ||
| 266 | + """ | ||
| 267 | + Perform Layer 2 safety check using to_dense() fast pre-filter. | ||
| 268 | + | ||
| 269 | + This checks if every Sparse Q-block has at least one valid KV at the | ||
| 270 | + Sparse Block level (32x32 elements per cell). Cost: ~1ms per mask. | ||
| 271 | + | ||
| 272 | + Args: | ||
| 273 | + block_mask: A PyTorch BlockMask object with to_dense() method | ||
| 274 | + | ||
| 275 | + Returns: | ||
| 276 | + tuple[bool, str]: (is_safe, detail_message) | ||
| 277 | + - is_safe: True if all rows have >=1 valid cell, False otherwise | ||
| 278 | + - detail_message: Human-readable result for logging | ||
| 279 | + | ||
| 280 | + Note: | ||
| 281 | + L2 has 40% false positive rate (granularity trap)! | ||
| 282 | + Must be combined with L3 whitelist for production safety. | ||
| 283 | + See v4.0 design doc: Level1_Level2_完整设计文档.md §2.1 | ||
| 284 | + """ | ||
| 285 | + try: | ||
| 286 | + import time as _time | ||
| 287 | + _t_start = _time.time() | ||
| 288 | + if log.isEnabledFor(logging.INFO): | ||
| 289 | + log.info( | ||
| 290 | + "[meta][L2] start block_mask_type=%s", | ||
| 291 | + type(block_mask).__name__, | ||
| 292 | + ) | ||
| 293 | + | ||
| 294 | + dense = block_mask.to_dense() | ||
| 295 | + dense_device = getattr(dense, "device", "<unknown>") | ||
| 296 | + dense_dtype = getattr(dense, "dtype", "<unknown>") | ||
| 297 | + dense_shape = tuple(dense.shape) if hasattr(dense, "shape") else "<unknown>" | ||
| 298 | + if log.isEnabledFor(logging.INFO): | ||
| 299 | + log.info( | ||
| 300 | + "[meta][L2] dense_ready shape=%s dense_device=%s dtype=%s elapsed=%.2fms", | ||
| 301 | + dense_shape, | ||
| 302 | + dense_device, | ||
| 303 | + dense_dtype, | ||
| 304 | + (_time.time() - _t_start) * 1000, | ||
| 305 | + ) | ||
| 306 | + | ||
| 307 | + b, h = 0, 0 | ||
| 308 | + dense_2d = dense[b, h] | ||
| 309 | + | ||
| 310 | + row_has_valid = dense_2d.any(dim=-1) | ||
| 311 | + l2_safe = row_has_valid.all().item() | ||
| 312 | + | ||
| 313 | + _elapsed = (_time.time() - _t_start) * 1000 | ||
| 314 | + | ||
| 315 | + if l2_safe: | ||
| 316 | + valid_rows = row_has_valid.sum().item() | ||
| 317 | + total_rows = len(row_has_valid) | ||
| 318 | + detail = f"L2 SAFE ({valid_rows}/{total_rows} rows valid, {_elapsed:.2f}ms)" | ||
| 319 | + if log.isEnabledFor(logging.INFO): | ||
| 320 | + log.info( | ||
| 321 | + "[meta][L2] done safe=True valid_rows=%s total_rows=%s " | ||
| 322 | + "dense_device=%s elapsed=%.2fms", | ||
| 323 | + valid_rows, | ||
| 324 | + total_rows, | ||
| 325 | + dense_device, | ||
| 326 | + _elapsed, | ||
| 327 | + ) | ||
| 328 | + return True, detail | ||
| 329 | + else: | ||
| 330 | + unsafe_rows = (~row_has_valid).nonzero(as_tuple=True)[0].tolist() | ||
| 331 | + detail = f"L2 UNSAFE ({len(unsafe_rows)} empty rows: {unsafe_rows[:10]}, {_elapsed:.2f}ms)" | ||
| 332 | + if log.isEnabledFor(logging.INFO): | ||
| 333 | + log.info( | ||
| 334 | + "[meta][L2] done safe=False unsafe_rows=%s total_rows=%s " | ||
| 335 | + "dense_device=%s elapsed=%.2fms", | ||
| 336 | + unsafe_rows[:20], | ||
| 337 | + len(row_has_valid), | ||
| 338 | + dense_device, | ||
| 339 | + _elapsed, | ||
| 340 | + ) | ||
| 341 | + return False, detail | ||
| 342 | + | ||
| 343 | + except Exception as exc: | ||
| 344 | + if log.isEnabledFor(logging.INFO): | ||
| 345 | + log.info( # noqa: G200 | ||
| 346 | + "[meta][L2] done safe=False error=%s: %s", | ||
| 347 | + type(exc).__name__, | ||
| 348 | + exc, | ||
| 349 | + ) | ||
| 350 | + return False, f"L2 ERROR: {type(exc).__name__}: {exc}" | ||
| 351 | + | ||
| 352 | + | ||
| 353 | +def _get_critical_positions(seqlen: int, block_size: int = 32, sparse_block_size: int = 128) -> list[int]: | ||
| 354 | + """ | ||
| 355 | + Generate critical boundary positions for Element-Level safety sampling. | ||
| 356 | + | ||
| 357 | + Instead of scanning all N positions (O(N^2)), we sample key boundary | ||
| 358 | + locations where granularity traps are most likely to occur: | ||
| 359 | + - Sparse Block boundaries (multiples of 128) | ||
| 360 | + - Mask Block boundaries (multiples of 32) | ||
| 361 | + - First/last positions of each region | ||
| 362 | + - Segment boundaries if applicable | ||
| 363 | + | ||
| 364 | + This reduces verification from O(N^2) to O(N) while maintaining high accuracy. | ||
| 365 | + Based on v3.0 empirical validation (526 positions sufficient for seqlen=8192). | ||
| 366 | + | ||
| 367 | + Args: | ||
| 368 | + seqlen: Total sequence length | ||
| 369 | + block_size: Mask Block size (default 32) | ||
| 370 | + sparse_block_size: Sparse Block size (default 128) | ||
| 371 | + | ||
| 372 | + Returns: | ||
| 373 | + list[int]: Sorted list of critical query positions to check | ||
| 374 | + """ | ||
| 375 | + positions = set() | ||
| 376 | + | ||
| 377 | + num_sparse_blocks = (seqlen + sparse_block_size - 1) // sparse_block_size | ||
| 378 | + num_blocks_per_sparse = sparse_block_size // block_size | ||
| 379 | + | ||
| 380 | + for sb in range(num_sparse_blocks): | ||
| 381 | + sb_start = sb * sparse_block_size | ||
| 382 | + sb_end = min(sb_start + sparse_block_size, seqlen) | ||
| 383 | + | ||
| 384 | + positions.add(sb_start) | ||
| 385 | + positions.add(min(sb_end - 1, seqlen - 1)) | ||
| 386 | + | ||
| 387 | + for bi in range(num_blocks_per_sparse): | ||
| 388 | + b_start = sb_start + bi * block_size | ||
| 389 | + b_end = min(b_start + block_size, seqlen) | ||
| 390 | + if b_start < seqlen: | ||
| 391 | + positions.add(b_start) | ||
| 392 | + if b_end - 1 < seqlen and b_end - 1 >= 0: | ||
| 393 | + positions.add(b_end - 1) | ||
| 394 | + | ||
| 395 | + mid = (sb_start + sb_end) // 2 | ||
| 396 | + if mid < seqlen: | ||
| 397 | + positions.add(mid) | ||
| 398 | + | ||
| 399 | + for i in range(0, min(10, seqlen)): | ||
| 400 | + positions.add(i) | ||
| 401 | + | ||
| 402 | + for i in range(max(0, seqlen - 10), seqlen): | ||
| 403 | + positions.add(i) | ||
| 404 | + | ||
| 405 | + return sorted([p for p in positions if p < seqlen]) | ||
| 406 | + | ||
| 407 | + | ||
| 408 | +def has_any_valid_kv(mask_fn: Any, q_pos: int, seqlen: int, b: int = 0, h: int = 0) -> bool: | ||
| 409 | + """ | ||
| 410 | + Check if a specific query position has at least one valid KV position. | ||
| 411 | + | ||
| 412 | + This is the core Element-Level (1x1) safety check. For a given query, | ||
| 413 | + it scans all possible KVs to find at least one valid (q,k) pair according | ||
| 414 | + to the mask_mod function. | ||
| 415 | + | ||
| 416 | + Args: | ||
| 417 | + mask_fn: The mask_mod function from create_block_mask() | ||
| 418 | + q_pos: Query position to check (element-level index) | ||
| 419 | + seqlen: Total sequence length | ||
| 420 | + b: Batch index (default 0) | ||
| 421 | + h: Head index (default 0) | ||
| 422 | + | ||
| 423 | + Returns: | ||
| 424 | + bool: True if query has >=1 valid KV, False otherwise | ||
| 425 | + | ||
| 426 | + Note: | ||
| 427 | + This is the definitive safety check that eliminates granularity traps. | ||
| 428 | + A return of False means this query would produce NaN in attention computation. | ||
| 429 | + """ | ||
| 430 | + try: | ||
| 431 | + device = torch.device("cpu") | ||
| 432 | + b_idx = torch.tensor(b, device=device) | ||
| 433 | + h_idx = torch.tensor(h, device=device) | ||
| 434 | + q_idx = torch.tensor(q_pos, device=device) | ||
| 435 | + for kv_start in range(0, seqlen, 8192): | ||
| 436 | + kv_idx = torch.arange(kv_start, min(kv_start + 8192, seqlen), device=device) | ||
| 437 | + result = mask_fn(b_idx, h_idx, q_idx, kv_idx) | ||
| 438 | + result_tensor = ( | ||
| 439 | + result.to(dtype=torch.bool) | ||
| 440 | + if isinstance(result, torch.Tensor) | ||
| 441 | + else torch.as_tensor(result, dtype=torch.bool, device=device) | ||
| 442 | + ) | ||
| 443 | + if bool(result_tensor.any().item()): | ||
| 444 | + return True | ||
| 445 | + return False | ||
| 446 | + except Exception: | ||
| 447 | + return False | ||
| 448 | + | ||
| 449 | + | ||
| 450 | +def _infer_block_mask_seq_lengths(block_mask: Any) -> tuple[int, int] | None: | ||
| 451 | + seq_lengths = getattr(block_mask, "seq_lengths", None) | ||
| 452 | + if ( | ||
| 453 | + isinstance(seq_lengths, tuple) | ||
| 454 | + and len(seq_lengths) == 2 | ||
| 455 | + and all(isinstance(length, int) for length in seq_lengths) | ||
| 456 | + ): | ||
| 457 | + return seq_lengths | ||
| 458 | + return None | ||
| 459 | + | ||
| 460 | + | ||
| 461 | +def _infer_block_mask_batch_heads(block_mask: Any, counts: torch.Tensor | None) -> tuple[int, int]: | ||
| 462 | + if counts is not None and counts.ndim >= 3: | ||
| 463 | + return int(counts.shape[0]), int(counts.shape[1]) | ||
| 464 | + shape = getattr(block_mask, "shape", None) | ||
| 465 | + if isinstance(shape, tuple) and len(shape) >= 4: | ||
| 466 | + return int(shape[0]), int(shape[1]) | ||
| 467 | + return 1, 1 | ||
| 468 | + | ||
| 469 | + | ||
| 470 | +def _verify_rows_have_valid_kv_tensorized( | ||
| 471 | + mask_fn: Any, | ||
| 472 | + *, | ||
| 473 | + batch_size: int, | ||
| 474 | + num_heads: int, | ||
| 475 | + q_len: int, | ||
| 476 | + kv_len: int, | ||
| 477 | + device: torch.device, | ||
| 478 | + q_chunk_size: int = 256, | ||
| 479 | + kv_chunk_size: int = 8192, | ||
| 480 | + max_unsafe: int = 100, | ||
| 481 | +) -> tuple[bool, list[str]]: | ||
| 482 | + import time as _time | ||
| 483 | + _t_start = _time.time() | ||
| 484 | + unsafe_locations: list[str] = [] | ||
| 485 | + q_chunks_per_head = (q_len + q_chunk_size - 1) // q_chunk_size | ||
| 486 | + kv_chunks_per_q_chunk = (kv_len + kv_chunk_size - 1) // kv_chunk_size | ||
| 487 | + total_q_chunks = batch_size * num_heads * q_chunks_per_head | ||
| 488 | + progress_log_every = max(1, total_q_chunks // 16) | ||
| 489 | + q_chunks_done = 0 | ||
| 490 | + result_device_warned = False | ||
| 491 | + | ||
| 492 | + if log.isEnabledFor(logging.INFO): | ||
| 493 | + log.info( | ||
| 494 | + "[meta][L3] scan_start batch=%d heads=%d q_len=%d kv_len=%d " | ||
| 495 | + "device=%s q_chunk_size=%d kv_chunk_size=%d total_q_chunks=%d " | ||
| 496 | + "kv_chunks_per_q_chunk=%d max_unsafe=%d", | ||
| 497 | + batch_size, | ||
| 498 | + num_heads, | ||
| 499 | + q_len, | ||
| 500 | + kv_len, | ||
| 501 | + device, | ||
| 502 | + q_chunk_size, | ||
| 503 | + kv_chunk_size, | ||
| 504 | + total_q_chunks, | ||
| 505 | + kv_chunks_per_q_chunk, | ||
| 506 | + max_unsafe, | ||
| 507 | + ) | ||
| 508 | + | ||
| 509 | + for b_idx_value in range(batch_size): | ||
| 510 | + b_idx = torch.tensor(b_idx_value, device=device) | ||
| 511 | + for h_idx_value in range(num_heads): | ||
| 512 | + h_idx = torch.tensor(h_idx_value, device=device) | ||
| 513 | + for q_start in range(0, q_len, q_chunk_size): | ||
| 514 | + q_end = min(q_start + q_chunk_size, q_len) | ||
| 515 | + q_idx = torch.arange(q_start, q_end, device=device)[:, None] | ||
| 516 | + row_has_valid = torch.zeros(q_end - q_start, dtype=torch.bool, device=device) | ||
| 517 | + | ||
| 518 | + for kv_start in range(0, kv_len, kv_chunk_size): | ||
| 519 | + kv_end = min(kv_start + kv_chunk_size, kv_len) | ||
| 520 | + kv_idx = torch.arange(kv_start, kv_end, device=device)[None, :] | ||
| 521 | + result = mask_fn(b_idx, h_idx, q_idx, kv_idx) | ||
| 522 | + result_tensor = ( | ||
| 523 | + result.to(dtype=torch.bool) | ||
| 524 | + if isinstance(result, torch.Tensor) | ||
| 525 | + else torch.as_tensor(result, dtype=torch.bool, device=device) | ||
| 526 | + ) | ||
| 527 | + result_device = getattr(result_tensor, "device", None) | ||
| 528 | + if ( | ||
| 529 | + not result_device_warned | ||
| 530 | + and result_device is not None | ||
| 531 | + and str(result_device) != str(device) | ||
| 532 | + ): | ||
| 533 | + log.warning( | ||
| 534 | + "[meta][L3] mask_mod result device=%s differs from " | ||
| 535 | + "analysis device=%s; metadata verification may not be " | ||
| 536 | + "using the expected accelerated path", | ||
| 537 | + result_device, | ||
| 538 | + device, | ||
| 539 | + ) | ||
| 540 | + result_device_warned = True | ||
| 541 | + if result_tensor.ndim == 0: | ||
| 542 | + result_tensor = result_tensor.expand(q_end - q_start, kv_end - kv_start) | ||
| 543 | + elif tuple(result_tensor.shape) != (q_end - q_start, kv_end - kv_start): | ||
| 544 | + result_tensor = torch.broadcast_to( | ||
| 545 | + result_tensor, | ||
| 546 | + (q_end - q_start, kv_end - kv_start), | ||
| 547 | + ) | ||
| 548 | + row_has_valid |= result_tensor.any(dim=1) | ||
| 549 | + if bool(row_has_valid.all().item()): | ||
| 550 | + break | ||
| 551 | + | ||
| 552 | + q_chunks_done += 1 | ||
| 553 | + if ( | ||
| 554 | + log.isEnabledFor(logging.INFO) | ||
| 555 | + and ( | ||
| 556 | + q_chunks_done == 1 | ||
| 557 | + or q_chunks_done % progress_log_every == 0 | ||
| 558 | + or q_chunks_done == total_q_chunks | ||
| 559 | + ) | ||
| 560 | + ): | ||
| 561 | + valid_rows = int(row_has_valid.sum().item()) | ||
| 562 | + log.info( | ||
| 563 | + "[meta][L3] progress q_chunk=%d/%d b=%d h=%d " | ||
| 564 | + "q_range=[%d,%d) valid_rows=%d/%d unsafe_seen=%d " | ||
| 565 | + "elapsed=%.2fms device=%s", | ||
| 566 | + q_chunks_done, | ||
| 567 | + total_q_chunks, | ||
| 568 | + b_idx_value, | ||
| 569 | + h_idx_value, | ||
| 570 | + q_start, | ||
| 571 | + q_end, | ||
| 572 | + valid_rows, | ||
| 573 | + q_end - q_start, | ||
| 574 | + len(unsafe_locations), | ||
| 575 | + (_time.time() - _t_start) * 1000, | ||
| 576 | + device, | ||
| 577 | + ) | ||
| 578 | + | ||
| 579 | + if not bool(row_has_valid.all().item()): | ||
| 580 | + bad_rows = (~row_has_valid).nonzero(as_tuple=True)[0].to("cpu").tolist() | ||
| 581 | + for bad_row in bad_rows: | ||
| 582 | + unsafe_locations.append( | ||
| 583 | + f"b={b_idx_value},h={h_idx_value},q={q_start + int(bad_row)}" | ||
| 584 | + ) | ||
| 585 | + if len(unsafe_locations) >= max_unsafe: | ||
| 586 | + if log.isEnabledFor(logging.INFO): | ||
| 587 | + log.info( | ||
| 588 | + "[meta][L3] stop max_unsafe=%d reached " | ||
| 589 | + "q_chunk=%d/%d elapsed=%.2fms device=%s", | ||
| 590 | + max_unsafe, | ||
| 591 | + q_chunks_done, | ||
| 592 | + total_q_chunks, | ||
| 593 | + (_time.time() - _t_start) * 1000, | ||
| 594 | + device, | ||
| 595 | + ) | ||
| 596 | + return False, unsafe_locations | ||
| 597 | + | ||
| 598 | + if log.isEnabledFor(logging.INFO): | ||
| 599 | + log.info( | ||
| 600 | + "[meta][L3] done safe=%s checked_q_chunks=%d/%d unsafe_count=%d " | ||
| 601 | + "elapsed=%.2fms device=%s", | ||
| 602 | + len(unsafe_locations) == 0, | ||
| 603 | + q_chunks_done, | ||
| 604 | + total_q_chunks, | ||
| 605 | + len(unsafe_locations), | ||
| 606 | + (_time.time() - _t_start) * 1000, | ||
| 607 | + device, | ||
| 608 | + ) | ||
| 609 | + return len(unsafe_locations) == 0, unsafe_locations | ||
| 610 | + | ||
| 611 | + | ||
| 612 | +def _verify_element_level_safety(block_mask: Any) -> tuple[bool, str]: | ||
| 613 | + """ | ||
| 614 | + Perform TRUE Element-Level (1x1) online safety verification. | ||
| 615 | + | ||
| 616 | + This replaces static whitelist lookup with dynamic runtime analysis. | ||
| 617 | + For each critical query position, it checks whether there exists at least | ||
| 618 | + one valid KV using the actual mask_mod function. | ||
| 619 | + | ||
| 620 | + Based on v3.0 methodology (verify_all_masks_element_level.py). | ||
| 621 | + Samples ~526 critical boundary positions instead of full O(N^2) scan. | ||
| 622 | + | ||
| 623 | + Args: | ||
| 624 | + block_mask: A PyTorch BlockMask object with mask_mod attribute | ||
| 625 | + | ||
| 626 | + Returns: | ||
| 627 | + tuple[bool, str]: (is_safe, detail_message) | ||
| 628 | + - is_safe: True if ALL checked queries have valid KVs | ||
| 629 | + - detail_message: Human-readable result including unsafe count | ||
| 630 | + | ||
| 631 | + Performance: | ||
| 632 | + - Uses tensorized q_chunk x kv_chunk row reductions. | ||
| 633 | + - Runs on the BlockMask tensor device, so eager NPU BlockMask creation | ||
| 634 | + can perform this check on NPU and cache the result. | ||
| 635 | + """ | ||
| 636 | + import time as _time | ||
| 637 | + _t_start = _time.time() | ||
| 638 | + | ||
| 639 | + try: | ||
| 640 | + mask_mod_fn = getattr(block_mask, 'mask_mod', None) | ||
| 641 | + kv_num_blocks_attr = getattr(block_mask, 'kv_num_blocks', None) | ||
| 642 | + | ||
| 643 | + if mask_mod_fn is None or kv_num_blocks_attr is None: | ||
| 644 | + elapsed = (_time.time() - _t_start) * 1000 | ||
| 645 | + return False, f"L3 ERROR: missing mask_mod/kv_num_blocks ({elapsed:.2f}ms)" | ||
| 646 | + | ||
| 647 | + counts = _try_unwrap_tensor(kv_num_blocks_attr) | ||
| 648 | + if counts is None: | ||
| 649 | + elapsed = (_time.time() - _t_start) * 1000 | ||
| 650 | + return False, f"L3 ERROR: cannot unwrap kv_num_blocks ({elapsed:.2f}ms)" | ||
| 651 | + | ||
| 652 | + seq_lengths = _infer_block_mask_seq_lengths(block_mask) | ||
| 653 | + if seq_lengths is None: | ||
| 654 | + elapsed = (_time.time() - _t_start) * 1000 | ||
| 655 | + return False, f"L3 ERROR: missing seq_lengths ({elapsed:.2f}ms)" | ||
| 656 | + q_len, kv_len = seq_lengths | ||
| 657 | + | ||
| 658 | + if q_len <= 0 or kv_len <= 0 or q_len > 100000 or kv_len > 100000: | ||
| 659 | + elapsed = (_time.time() - _t_start) * 1000 | ||
| 660 | + return False, f"L3 ERROR: invalid seq_lengths={seq_lengths} ({elapsed:.2f}ms)" | ||
| 661 | + | ||
| 662 | + batch_size, num_heads = _infer_block_mask_batch_heads(block_mask, counts) | ||
| 663 | + if log.isEnabledFor(logging.INFO): | ||
| 664 | + log.info( | ||
| 665 | + "[meta][L3] start block_mask_type=%s batch=%d heads=%d " | ||
| 666 | + "q_len=%d kv_len=%d device=%s counts_shape=%s counts_dtype=%s", | ||
| 667 | + type(block_mask).__name__, | ||
| 668 | + batch_size, | ||
| 669 | + num_heads, | ||
| 670 | + q_len, | ||
| 671 | + kv_len, | ||
| 672 | + counts.device, | ||
| 673 | + tuple(counts.shape), | ||
| 674 | + counts.dtype, | ||
| 675 | + ) | ||
| 676 | + if counts.device.type != "npu": | ||
| 677 | + log.warning( | ||
| 678 | + "[meta][L3] running on CPU device=%s; NPU acceleration is not " | ||
| 679 | + "active for metadata row-safety verification", | ||
| 680 | + counts.device, | ||
| 681 | + ) | ||
| 682 | + is_safe, unsafe_locations = _verify_rows_have_valid_kv_tensorized( | ||
| 683 | + mask_mod_fn, | ||
| 684 | + batch_size=batch_size, | ||
| 685 | + num_heads=num_heads, | ||
| 686 | + q_len=q_len, | ||
| 687 | + kv_len=kv_len, | ||
| 688 | + device=counts.device, | ||
| 689 | + ) | ||
| 690 | + | ||
| 691 | + elapsed = (_time.time() - _t_start) * 1000 | ||
| 692 | + | ||
| 693 | + if is_safe: | ||
| 694 | + total_rows = batch_size * num_heads * q_len | ||
| 695 | + return True, f"L3 SAFE ({total_rows} rows verified exactly, {elapsed:.2f}ms)" | ||
| 696 | + else: | ||
| 697 | + return ( | ||
| 698 | + False, | ||
| 699 | + f"L3 UNSAFE ({len(unsafe_locations)} rows have no valid KV: " | ||
| 700 | + f"{unsafe_locations[:20]}..., {elapsed:.2f}ms)" | ||
| 701 | + ) | ||
| 702 | + | ||
| 703 | + except Exception as exc: | ||
| 704 | + elapsed = (_time.time() - _t_start) * 1000 | ||
| 705 | + return False, f"L3 EXCEPTION: {type(exc).__name__}: {exc} ({elapsed:.2f}ms)" | ||
| 706 | + | ||
| 707 | + | ||
| 708 | +def infer_eager_block_mask_kernel_options(block_mask: Any) -> dict[str, Any]: | ||
| 709 | + """Infer and cache eager BlockMask options before Dynamo graph capture.""" | ||
| 710 | + if block_mask is None: | ||
| 711 | + return {} | ||
| 712 | + | ||
| 713 | + options = _infer_sparse_mask_compact_options(block_mask) | ||
| 714 | + if not _metadata_auto_infer_enabled(): | ||
| 715 | + options["NPU_ROWS_GUARANTEED_SAFE"] = False | ||
| 716 | + options["NPU_BLOCKS_ARE_CONTIGUOUS"] = False | ||
| 717 | + return options | ||
| 718 | + | ||
| 719 | + kv_num_blocks, kv_indices = _extract_block_sparse_tensors(block_mask) | ||
| 720 | + if kv_num_blocks is None or kv_indices is None: | ||
| 721 | + return options | ||
| 722 | + | ||
| 723 | + rows_value, _ = _verify_element_level_safety(block_mask) | ||
| 724 | + options["NPU_ROWS_GUARANTEED_SAFE"] = rows_value | ||
| 725 | + | ||
| 726 | + contiguous_value = _infer_blocks_are_contiguous_from_tensors(kv_num_blocks, kv_indices) | ||
| 727 | + if contiguous_value != "Unknown": | ||
| 728 | + options["NPU_BLOCKS_ARE_CONTIGUOUS"] = contiguous_value | ||
| 729 | + return options | ||
| 730 | + | ||
| 731 | + | ||
| 732 | +def infer_block_sparse_metadata(kv_num_blocks: Any, kv_indices: Any, block_mask: Any = None) -> dict[str, Any]: | ||
| 733 | + """ | ||
| 734 | + Infer metadata from block-sparse structures using v4.0 two-layer architecture. | ||
| 735 | + | ||
| 736 | + Architecture (v4.0): | ||
| 737 | + - Layer 2: Fast Pre-filter via to_dense() (~1ms) - excludes obviously unsafe | ||
| 738 | + - Layer 3: Element-Level whitelist lookup (O(1)) - final safety decision | ||
| 739 | + | ||
| 740 | + This replaces the previous overly optimistic "Level 0.5" approach that had | ||
| 741 | + 40% false positive rate (causing NaN crashes in production). | ||
| 742 | + | ||
| 743 | + Args: | ||
| 744 | + kv_num_blocks: Row-level KV block counts tensor | ||
| 745 | + kv_indices: Row-level KV block index lists | ||
| 746 | + block_mask: (NEW) Optional BlockMask object for L2/L3 analysis | ||
| 747 | + | ||
| 748 | + Returns: | ||
| 749 | + dict with keys: | ||
| 750 | + - rows_guaranteed_safe: bool/"Unknown" (final verdict) | ||
| 751 | + - blocks_are_contiguous: bool/"Unknown" | ||
| 752 | + - is_per_head_heterogeneous: bool | ||
| 753 | + - empty_row_risk_level: "low"/"medium"/"high" | ||
| 754 | + - l2_result: str (Layer 2 analysis detail for debugging) | ||
| 755 | + - l3_result: str (Layer 3 whitelist lookup detail for debugging) | ||
| 756 | + - safety_layer: str ("L2_FAIL"/"L3_WHITELIST"/"L3_CONSERVATIVE"/"LEGACY") | ||
| 757 | + """ | ||
| 758 | + if not _metadata_auto_infer_enabled(): | ||
| 759 | + return { | ||
| 760 | + "rows_guaranteed_safe": "Unknown", | ||
| 761 | + "blocks_are_contiguous": "Unknown", | ||
| 762 | + "is_per_head_heterogeneous": False, | ||
| 763 | + "empty_row_risk_level": "medium", | ||
| 764 | + "l2_result": "DISABLED_BY_CONFIG", | ||
| 765 | + "l3_result": "DISABLED_BY_CONFIG", | ||
| 766 | + "safety_layer": "DISABLED", | ||
| 767 | + } | ||
| 768 | + | ||
| 769 | + if block_mask is None or not callable(getattr(block_mask, "to_dense", None)): | ||
| 770 | + log.warning( | ||
| 771 | + "[meta][v4.0] block_mask is missing to_dense(), falling back to legacy Level 0.5 method. " | ||
| 772 | + "This has 40% false positive rate! Pass block_mask for v4.0 safety." | ||
| 773 | + ) | ||
| 774 | + counts_tensor = _try_unwrap_tensor(kv_num_blocks) | ||
| 775 | + indices_tensor = _try_unwrap_tensor(kv_indices) | ||
| 776 | + if counts_tensor is None or indices_tensor is None: | ||
| 777 | + return { | ||
| 778 | + "rows_guaranteed_safe": "Unknown", | ||
| 779 | + "blocks_are_contiguous": "Unknown", | ||
| 780 | + "is_per_head_heterogeneous": False, | ||
| 781 | + "empty_row_risk_level": "medium", | ||
| 782 | + "l2_result": "NO_BLOCK_MASK", | ||
| 783 | + "l3_result": "NO_BLOCK_MASK", | ||
| 784 | + "safety_layer": "LEGACY_FALLBACK", | ||
| 785 | + } | ||
| 786 | + | ||
| 787 | + row_counts, row_indices = _normalize_block_rows(counts_tensor, indices_tensor) | ||
| 788 | + rows_guaranteed_safe_legacy = all(count > 0 for count in row_counts) | ||
| 789 | + blocks_are_contiguous = True | ||
| 790 | + for values in row_indices: | ||
| 791 | + if len(values) <= 1: | ||
| 792 | + continue | ||
| 793 | + if any((right - left) != 1 for left, right in zip(values, values[1:])): | ||
| 794 | + blocks_are_contiguous = False | ||
| 795 | + break | ||
| 796 | + | ||
| 797 | + headwise_counts = counts_tensor.to("cpu", dtype=torch.int64) | ||
| 798 | + headwise_indices = indices_tensor.to("cpu", dtype=torch.int64) | ||
| 799 | + is_per_head_heterogeneous = not _heads_share_used_block_entries( | ||
| 800 | + headwise_counts, headwise_indices | ||
| 801 | + ) | ||
| 802 | + | ||
| 803 | + return { | ||
| 804 | + "rows_guaranteed_safe": rows_guaranteed_safe_legacy, | ||
| 805 | + "blocks_are_contiguous": blocks_are_contiguous, | ||
| 806 | + "is_per_head_heterogeneous": is_per_head_heterogeneous, | ||
| 807 | + "empty_row_risk_level": "low" if rows_guaranteed_safe_legacy else "high", | ||
| 808 | + "l2_result": "SKIPPED_NO_BLOCK_MASK", | ||
| 809 | + "l3_result": "SKIPPED_NO_BLOCK_MASK", | ||
| 810 | + "safety_layer": "LEGACY_DEPRECATED", | ||
| 811 | + } | ||
| 812 | + | ||
| 813 | + l2_safe, l2_detail = _layer2_fast_prefilter(block_mask) | ||
| 814 | + | ||
| 815 | + if not l2_safe: | ||
| 816 | + counts_tensor = _try_unwrap_tensor(kv_num_blocks) | ||
| 817 | + indices_tensor = _try_unwrap_tensor(kv_indices) | ||
| 818 | + if counts_tensor is not None and indices_tensor is not None: | ||
| 819 | + row_counts, row_indices = _normalize_block_rows(counts_tensor, indices_tensor) | ||
| 820 | + blocks_are_contiguous = True | ||
| 821 | + for values in row_indices: | ||
| 822 | + if len(values) <= 1: | ||
| 823 | + continue | ||
| 824 | + if any((right - left) != 1 for left, right in zip(values, values[1:])): | ||
| 825 | + blocks_are_contiguous = False | ||
| 826 | + break | ||
| 827 | + else: | ||
| 828 | + blocks_are_contiguous = "Unknown" | ||
| 829 | + | ||
| 830 | + return { | ||
| 831 | + "rows_guaranteed_safe": False, | ||
| 832 | + "blocks_are_contiguous": blocks_are_contiguous, | ||
| 833 | + "is_per_head_heterogeneous": False, | ||
| 834 | + "empty_row_risk_level": "high", | ||
| 835 | + "l2_result": l2_detail, | ||
| 836 | + "l3_result": "SKIPPED_L2_FAIL", | ||
| 837 | + "safety_layer": "L2_FAIL_FAST_REJECT", | ||
| 838 | + } | ||
| 839 | + | ||
| 840 | + l3_safe, l3_detail = _verify_element_level_safety(block_mask) | ||
| 841 | + | ||
| 842 | + if l3_safe: | ||
| 843 | + counts_tensor = _try_unwrap_tensor(kv_num_blocks) | ||
| 844 | + indices_tensor = _try_unwrap_tensor(kv_indices) | ||
| 845 | + if counts_tensor is not None and indices_tensor is not None: | ||
| 846 | + row_counts, row_indices = _normalize_block_rows(counts_tensor, indices_tensor) | ||
| 847 | + blocks_are_contiguous = True | ||
| 848 | + for values in row_indices: | ||
| 849 | + if len(values) <= 1: | ||
| 850 | + continue | ||
| 851 | + if any((right - left) != 1 for left, right in zip(values, values[1:])): | ||
| 852 | + blocks_are_contiguous = False | ||
| 853 | + break | ||
| 854 | + else: | ||
| 855 | + blocks_are_contiguous = True | ||
| 856 | + | ||
| 857 | + return { | ||
| 858 | + "rows_guaranteed_safe": True, | ||
| 859 | + "blocks_are_contiguous": blocks_are_contiguous, | ||
| 860 | + "is_per_head_heterogeneous": False, | ||
| 861 | + "empty_row_risk_level": "low", | ||
| 862 | + "l2_result": l2_detail, | ||
| 863 | + "l3_result": f"ELEMENT_VERIFIED_SAFE({l3_detail})", | ||
| 864 | + "safety_layer": "L3_ELEMENT_PASS", | ||
| 865 | + } | ||
| 866 | + else: | ||
| 867 | + counts_tensor = _try_unwrap_tensor(kv_num_blocks) | ||
| 868 | + indices_tensor = _try_unwrap_tensor(kv_indices) | ||
| 869 | + if counts_tensor is not None and indices_tensor is not None: | ||
| 870 | + row_counts, row_indices = _normalize_block_rows(counts_tensor, indices_tensor) | ||
| 871 | + blocks_are_contiguous = True | ||
| 872 | + for values in row_indices: | ||
| 873 | + if len(values) <= 1: | ||
| 874 | + continue | ||
| 875 | + if any((right - left) != 1 for left, right in zip(values, values[1:])): | ||
| 876 | + blocks_are_contiguous = False | ||
| 877 | + break | ||
| 878 | + else: | ||
| 879 | + blocks_are_contiguous = "Unknown" | ||
| 880 | + | ||
| 881 | + return { | ||
| 882 | + "rows_guaranteed_safe": "Unknown", | ||
| 883 | + "blocks_are_contiguous": blocks_are_contiguous, | ||
| 884 | + "is_per_head_heterogeneous": False, | ||
| 885 | + "empty_row_risk_level": "medium", | ||
| 886 | + "l2_result": l2_detail, | ||
| 887 | + "l3_result": f"ELEMENT_UNSAFE({l3_detail})", | ||
| 888 | + "safety_layer": "L3_ELEMENT_FAIL", | ||
| 889 | + } | ||
| 890 | + | ||
| 891 | + | ||
| 892 | +def build_flex_attention_metadata(kv_num_blocks: Any, kv_indices: Any, block_mask: Any = None) -> dict[str, Any]: | ||
| 893 | + """Build the minimal metadata contract used by flex attention lowering.""" | ||
| 894 | + metadata = infer_block_sparse_metadata(kv_num_blocks, kv_indices, block_mask=block_mask) | ||
| 895 | + metadata.setdefault("primary_mask_type", "block_sparse") | ||
| 896 | + metadata.setdefault("is_segment_aware", "Unknown") | ||
| 897 | + metadata.setdefault("is_pad_safe", "Unknown") | ||
| 898 | + metadata.setdefault("causal_granularity", "Unknown") | ||
| 899 | + metadata.setdefault("is_approximate", False) | ||
| 900 | + metadata.setdefault("block_granularity_mode", "single_block_size") | ||
| 901 | + metadata.setdefault("has_cross_half_dependency", False) | ||
| 902 | + return metadata | ||
| 903 | + | ||
| 904 | + | ||
| 905 | +def _has_block_sparse_kernel_option_override(kernel_options: dict[str, Any]) -> bool: | ||
| 906 | + """Check whether block-sparse kernel options are already fully specified.""" | ||
| 907 | + return ( | ||
| 908 | + "ROWS_GUARANTEED_SAFE" in kernel_options | ||
| 909 | + and "BLOCKS_ARE_CONTIGUOUS" in kernel_options | ||
| 910 | + ) | ||
| 911 | + | ||
| 912 | + | ||
| 913 | +def _extract_block_sparse_tensors(block_mask: Any) -> tuple[Any, Any]: | ||
| 914 | + """Read block-sparse tensors from a BlockMask-like object.""" | ||
| 915 | + if block_mask is None: | ||
| 916 | + return None, None | ||
| 917 | + return getattr(block_mask, "kv_num_blocks", None), getattr(block_mask, "kv_indices", None) | ||
| 918 | + | ||
| 919 | + | ||
| 920 | +def apply_kernel_options_from_metadata( | ||
| 921 | + kernel_options: dict[str, Any], metadata: dict[str, Any] | ||
| 922 | +) -> dict[str, Any]: | ||
| 923 | + """Apply conservative kernel-option defaults derived from metadata.""" | ||
| 924 | + updated = dict(kernel_options) | ||
| 925 | + rows_value = metadata.get("rows_guaranteed_safe", "Unknown") | ||
| 926 | + contiguous_value = metadata.get("blocks_are_contiguous", "Unknown") | ||
| 927 | + is_per_head_heterogeneous = metadata.get("is_per_head_heterogeneous", False) | ||
| 928 | + is_approximate = metadata.get("is_approximate", False) | ||
| 929 | + block_granularity_mode = metadata.get("block_granularity_mode", "single_block_size") | ||
| 930 | + empty_row_risk_level = metadata.get("empty_row_risk_level", "medium") | ||
| 931 | + has_cross_half_dependency = metadata.get("has_cross_half_dependency", False) | ||
| 932 | + causal_granularity = metadata.get("causal_granularity", "Unknown") | ||
| 933 | + | ||
| 934 | + if "ROWS_GUARANTEED_SAFE" not in updated: | ||
| 935 | + if rows_value is False or empty_row_risk_level == "high": | ||
| 936 | + updated["ROWS_GUARANTEED_SAFE"] = False | ||
| 937 | + elif ( | ||
| 938 | + rows_value is True | ||
| 939 | + and not is_per_head_heterogeneous | ||
| 940 | + and not is_approximate | ||
| 941 | + and block_granularity_mode != "multi_block_size" | ||
| 942 | + and empty_row_risk_level == "low" | ||
| 943 | + ): | ||
| 944 | + updated["ROWS_GUARANTEED_SAFE"] = True | ||
| 945 | + | ||
| 946 | + if "BLOCKS_ARE_CONTIGUOUS" not in updated: | ||
| 947 | + if contiguous_value is False: | ||
| 948 | + updated["BLOCKS_ARE_CONTIGUOUS"] = False | ||
| 949 | + elif ( | ||
| 950 | + contiguous_value is True | ||
| 951 | + and not is_per_head_heterogeneous | ||
| 952 | + and not is_approximate | ||
| 953 | + and block_granularity_mode != "multi_block_size" | ||
| 954 | + and not has_cross_half_dependency | ||
| 955 | + and causal_granularity != "mixed" | ||
| 956 | + ): | ||
| 957 | + updated["BLOCKS_ARE_CONTIGUOUS"] = True | ||
| 958 | + | ||
| 959 | + return updated | ||
| 960 | + | ||
| 961 | + | ||
| 962 | +def apply_kernel_options_from_eager_block_mask( | ||
| 963 | + kernel_options: dict[str, Any] | None, | ||
| 964 | + block_mask: Any, | ||
| 965 | + context: str = "eager", | ||
| 966 | + *, | ||
| 967 | + allow_tensor_analysis: bool = True, | ||
| 968 | +) -> dict[str, Any]: | ||
| 969 | + """Infer kernel options from an eager BlockMask-like object when tensors are still available.""" | ||
| 970 | + updated = {} if kernel_options is None else dict(kernel_options) | ||
| 971 | + updated = _apply_sparse_mask_compact_options( | ||
| 972 | + updated, | ||
| 973 | + block_mask, | ||
| 974 | + context, | ||
| 975 | + allow_tensor_analysis=allow_tensor_analysis, | ||
| 976 | + ) | ||
| 977 | + updated = _apply_precomputed_block_sparse_safety_options( | ||
| 978 | + updated, | ||
| 979 | + block_mask, | ||
| 980 | + context, | ||
| 981 | + ) | ||
| 982 | + if not _metadata_auto_infer_enabled(): | ||
| 983 | + return _apply_disabled_metadata_defaults(updated) | ||
| 984 | + | ||
| 985 | + if not allow_tensor_analysis: | ||
| 986 | + return updated | ||
| 987 | + if _has_block_sparse_kernel_option_override(updated): | ||
| 988 | + return updated | ||
| 989 | + | ||
| 990 | + kv_num_blocks, kv_indices = _extract_block_sparse_tensors(block_mask) | ||
| 991 | + if kv_num_blocks is None or kv_indices is None: | ||
| 992 | + return updated | ||
| 993 | + | ||
| 994 | + return apply_kernel_options_from_block_sparse_mask( | ||
| 995 | + updated, | ||
| 996 | + kv_num_blocks, | ||
| 997 | + kv_indices, | ||
| 998 | + block_mask=block_mask, | ||
| 999 | + context=context, | ||
| 1000 | + ) | ||
| 1001 | + | ||
| 1002 | + | ||
| 1003 | +def apply_kernel_options_from_block_sparse_mask( | ||
| 1004 | + kernel_options: dict[str, Any] | None, | ||
| 1005 | + kv_num_blocks: Any, | ||
| 1006 | + kv_indices: Any, | ||
| 1007 | + block_mask: Any = None, | ||
| 1008 | + context: str = "unknown" | ||
| 1009 | +) -> dict[str, Any]: | ||
| 1010 | + """Infer block-sparse metadata and conservatively merge it into kernel options.""" | ||
| 1011 | + updated = {} if kernel_options is None else dict(kernel_options) | ||
| 1012 | + if block_mask is not None: | ||
| 1013 | + updated = _apply_sparse_mask_compact_options( | ||
| 1014 | + updated, | ||
| 1015 | + block_mask, | ||
| 1016 | + context, | ||
| 1017 | + allow_tensor_analysis=True, | ||
| 1018 | + ) | ||
| 1019 | + updated = _apply_precomputed_block_sparse_safety_options( | ||
| 1020 | + updated, | ||
| 1021 | + block_mask, | ||
| 1022 | + context, | ||
| 1023 | + ) | ||
| 1024 | + if not _metadata_auto_infer_enabled(): | ||
| 1025 | + return _apply_disabled_metadata_defaults(updated) | ||
| 1026 | + | ||
| 1027 | + if _has_block_sparse_kernel_option_override(updated): | ||
| 1028 | + return updated | ||
| 1029 | + | ||
| 1030 | + metadata = build_flex_attention_metadata(kv_num_blocks, kv_indices, block_mask=block_mask) | ||
| 1031 | + | ||
| 1032 | + updated = apply_kernel_options_from_metadata(updated, metadata) | ||
| 1033 | + | ||
| 1034 | + if log.isEnabledFor(logging.INFO): | ||
| 1035 | + log.info( | ||
| 1036 | + "[flex_attention][%s] rows_guaranteed_safe=%s blocks_are_contiguous=%s " | ||
| 1037 | + "is_per_head_heterogeneous=%s empty_row_risk_level=%s " | ||
| 1038 | + "ROWS_GUARANTEED_SAFE=%s BLOCKS_ARE_CONTIGUOUS=%s " | ||
| 1039 | + "safety_layer=%s l2_result=%s l3_result=%s", | ||
| 1040 | + context, | ||
| 1041 | + metadata.get("rows_guaranteed_safe", "Unknown"), | ||
| 1042 | + metadata.get("blocks_are_contiguous", "Unknown"), | ||
| 1043 | + metadata.get("is_per_head_heterogeneous", False), | ||
| 1044 | + metadata.get("empty_row_risk_level", "medium"), | ||
| 1045 | + updated.get("ROWS_GUARANTEED_SAFE", "<unset>"), | ||
| 1046 | + updated.get("BLOCKS_ARE_CONTIGUOUS", "<unset>"), | ||
| 1047 | + metadata.get("safety_layer", "UNKNOWN"), | ||
| 1048 | + metadata.get("l2_result", "N/A"), | ||
| 1049 | + metadata.get("l3_result", "N/A"), | ||
| 1050 | + ) | ||
| 1051 | + return updated | ||