Draft
[WIP]Draft: perf(dsl) 单算子 RUN 包支持 Native AOT 并行 #6
zyh_gitcodeAdd on 8 days ago
[WIP]Draft: perf(dsl) 单算子 RUN 包支持 Native AOT 并行 #6
Draft
zyh_gitcodeAdd on 8 days ago
Total: 5 files changed+162-87
@@ -38,5 +38,5 @@ if find_spec("cannbotdsl") is not None and find_spec("opkit") is not None:
38endif()38endif()
39 39 
40if(BUILD_OPEN_PROJECT)40if(BUILD_OPEN_PROJECT)
41- enable_cannbotdsl_kernel(mixed_quant_sparse_flash_mla "${CMAKE_CURRENT_LIST_DIR}")41+ enable_cannbotdsl_kernel(mixed_quant_sparse_flash_mla "${CMAKE_CURRENT_LIST_DIR}" PARALLEL)
42endif()42endif()
@@ -9,7 +9,7 @@
9import math9import math
10import os10import os
11import threading11import threading
12-from itertools import product12+from itertools import islice, product
13from types import SimpleNamespace13from types import SimpleNamespace
14import cannbotdsl14import cannbotdsl
15import torch15import torch
@@ -3813,74 +3813,57 @@ def mixed_quant_sparse_flash_mla(
3813 return out, lse3813 return out, lse
3814 3814 
3815 3815 
3816+def _aot_profiles():
3817+ for tnd_padding, has_sinks, head_bucket in product((False, True), (True, False), (64, 32, 63)):
3818+ for has_cmp in (True, False):
3819+ cmp_lengths = (True, False) if has_cmp else (False,)
3820+ for has_ori_length, has_cmp_length, has_lse, addr_mode in product(
3821+ (True, False), cmp_lengths, (True, False), ("pre", "off", "arena")
3822+ ):
3823+ for pa_fast in ((False,) if addr_mode == "off" else (False, True)):
3824+ yield dict(
3825+ has_cmp=has_cmp,
3826+ has_ori_length=has_ori_length,
3827+ has_cmp_length=has_cmp_length,
3828+ has_lse=has_lse,
3829+ addr_mode=addr_mode,
3830+ layout_kv="PA_BBND",
3831+ pa_fast=pa_fast,
3832+ head_bucket=head_bucket,
3833+ has_sinks=has_sinks,
3834+ tnd_padding=tnd_padding,
3835+ )
3836+ for has_cmp in (True, False):
3837+ cmp_lengths = (True, False) if has_cmp else (False,)
3838+ for has_ori_length, has_cmp_length, has_lse, layout_kv in product(
3839+ (True, False), cmp_lengths, (True, False), ("TND", "BSND")
3840+ ):
3841+ yield dict(
3842+ has_cmp=has_cmp,
3843+ has_ori_length=has_ori_length,
3844+ has_cmp_length=has_cmp_length,
3845+ has_lse=has_lse,
3846+ addr_mode="off",
3847+ layout_kv=layout_kv,
3848+ head_bucket=head_bucket,
3849+ has_sinks=has_sinks,
3850+ tnd_padding=tnd_padding,
3851+ )
3852+ 
3853+ 
3854+def _export_mqsmla_shard(index, count):
3855+ clear_caches()
3856+ try:
3857+ for profile in islice(_aot_profiles(), index, None, count):
3858+ _get_compiled_variant(**profile)
3859+ finally:
3860+ clear_caches()
3861+ 
3862+ 
3816@aot.export("mixed_quant_sparse_flash_mla_dsl")3863@aot.export("mixed_quant_sparse_flash_mla_dsl")
3817def export_mqsmla():3864def export_mqsmla():
3818 """Collect all optional-input/layout/addressing variants with dynamic shapes."""3865 """Collect all optional-input/layout/addressing variants with dynamic shapes."""
3819- clear_caches()3866+ _export_mqsmla_shard(0, 1)
3820- try:3867+ 
3821- for tnd_padding in (False, True):3868+ 
3822- for has_sinks in (True, False):3869+export_mqsmla.__transformer_aot_shard__ = _export_mqsmla_shard
3823- for head_bucket in (64, 32, 63):
3824- for has_cmp in (True, False):
3825- cmp_lengths = (True, False) if has_cmp else (False,)
3826- for (
3827- has_ori_length,
3828- has_cmp_length,
3829- has_lse,
3830- addr_mode,
3831- ) in product(
3832- (True, False),
3833- cmp_lengths,
3834- (True, False),
3835- ("pre", "off", "arena"),
3836- ):
3837- _get_compiled_variant(
3838- has_cmp,
3839- has_ori_length,
3840- has_cmp_length,
3841- has_lse,
3842- addr_mode,
3843- layout_kv="PA_BBND",
3844- head_bucket=head_bucket,
3845- has_sinks=has_sinks,
3846- tnd_padding=tnd_padding,
3847- )
3848- if addr_mode != "off":
3849- _get_compiled_variant(
3850- has_cmp,
3851- has_ori_length,
3852- has_cmp_length,
3853- has_lse,
3854- addr_mode,
3855- layout_kv="PA_BBND",
3856- head_bucket=head_bucket,
3857- has_sinks=has_sinks,
3858- tnd_padding=tnd_padding,
3859- pa_fast=True,
3860- )
3861- for has_cmp in (True, False):
3862- cmp_lengths = (True, False) if has_cmp else (False,)
3863- for (
3864- has_ori_length,
3865- has_cmp_length,
3866- has_lse,
3867- layout_kv,
3868- ) in product(
3869- (True, False),
3870- cmp_lengths,
3871- (True, False),
3872- ("TND", "BSND"),
3873- ):
3874- _get_compiled_variant(
3875- has_cmp,
3876- has_ori_length,
3877- has_cmp_length,
3878- has_lse,
3879- "off",
3880- layout_kv,
3881- head_bucket=head_bucket,
3882- has_sinks=has_sinks,
3883- tnd_padding=tnd_padding,
3884- )
3885- finally:
3886- clear_caches()
@@ -2221,6 +2221,10 @@ if [ -n "${INPUT_JOB_NUM}" ]; then
2221 JOB_NUM="-j${INPUT_JOB_NUM}"2221 JOB_NUM="-j${INPUT_JOB_NUM}"
2222fi2222fi
2223 2223 
2224+if [[ "$ENABLE_BUILD_PKG" == "TRUE" && "${JOB_NUM}" =~ ^-j([0-9]+)$ ]]; then
2225+ CUSTOM_OPTION="${CUSTOM_OPTION} -DCANNBOTDSL_BUILD_JOBS=${BASH_REMATCH[1]}"
2226+fi
2227+ 
2224# 非打包命令调用,打包模式会打进同一个包里2228# 非打包命令调用,打包模式会打进同一个包里
2225function set_compute_unit_option() {2229function set_compute_unit_option() {
2226 # 使用共享校验库逐项校验 SoC 列表(支持逗号/分号分隔的多个 soc), 替换原有的整串精确匹配2230 # 使用共享校验库逐项校验 SoC 列表(支持逗号/分号分隔的多个 soc), 替换原有的整串精确匹配
@@ -804,6 +804,12 @@ function(enable_cannbotdsl_kernel op_name source_dir)
804 file(STRINGS "${_root}/inputs.list" _dependencies ENCODING UTF-8)804 file(STRINGS "${_root}/inputs.list" _dependencies ENCODING UTF-8)
805 set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${_dependencies}")805 set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${_dependencies}")
806 include("${_root}/inputs.cmake")806 include("${_root}/inputs.cmake")
807+ set(_native_build_args)
808+ # Only a single selected operator owns this build's DSL worker budget.
809+ if(ENABLE_BUILD_PKG AND _dsl_native AND NOT _dsl_aclnn AND "${ASCEND_OP_NAME}" STREQUAL "${op_name}" AND
810+ "${ARGV2}" STREQUAL "PARALLEL" AND CANNBOTDSL_BUILD_JOBS GREATER 1)
811+ set(_native_build_args --jobs "${CANNBOTDSL_BUILD_JOBS}")
812+ endif()
807 if(NOT _dsl_native AND NOT _dsl_aclnn)813 if(NOT _dsl_native AND NOT _dsl_aclnn)
808 message(FATAL_ERROR "No Native or ACLNN export found in ${_source}")814 message(FATAL_ERROR "No Native or ACLNN export found in ${_source}")
809 endif()815 endif()
@@ -826,7 +832,7 @@ function(enable_cannbotdsl_kernel op_name source_dir)
826 if(ENABLE_OPS_HOST AND _dsl_native)832 if(ENABLE_OPS_HOST AND _dsl_native)
827 # Imported archive targets and runtime sources must exist at configure time.833 # Imported archive targets and runtime sources must exist at configure time.
828 execute_process(COMMAND "${CMAKE_COMMAND}" -E env ${_env}834 execute_process(COMMAND "${CMAKE_COMMAND}" -E env ${_env}
829- "${HI_PYTHON}" "${_builder}" ${_target_args} --mode host --source "${_source}" --output "${_root}/host"835+ "${HI_PYTHON}" "${_builder}" ${_target_args} ${_native_build_args} --mode host --source "${_source}" --output "${_root}/host"
830 --inputs "${_inputs}"836 --inputs "${_inputs}"
831 RESULT_VARIABLE _result)837 RESULT_VARIABLE _result)
832 if(NOT _result EQUAL 0)838 if(NOT _result EQUAL 0)
@@ -876,7 +882,7 @@ function(enable_cannbotdsl_kernel op_name source_dir)
876 set(_stamp "${_root}/kernel.built")882 set(_stamp "${_root}/kernel.built")
877 add_custom_command(OUTPUT "${_stamp}"883 add_custom_command(OUTPUT "${_stamp}"
878 COMMAND "${CMAKE_COMMAND}" -E env ${_env}884 COMMAND "${CMAKE_COMMAND}" -E env ${_env}
879- "${HI_PYTHON}" "${_builder}" ${_target_args} --mode kernel --source "${_source}"885+ "${HI_PYTHON}" "${_builder}" ${_target_args} ${_native_build_args} --mode kernel --source "${_source}"
880 --output "${_root}/device" --kernel-output "${ASCEND_BINARY_OUT_DIR}/${CANNBOTDSL_COMPUTE_UNIT}/bin"886 --output "${_root}/device" --kernel-output "${ASCEND_BINARY_OUT_DIR}/${CANNBOTDSL_COMPUTE_UNIT}/bin"
881 --inputs "${_inputs}"887 --inputs "${_inputs}"
882 COMMAND "${CMAKE_COMMAND}" -E touch "${_stamp}"888 COMMAND "${CMAKE_COMMAND}" -E touch "${_stamp}"
@@ -13,16 +13,19 @@ import argparse
13import fcntl13import fcntl
14import hashlib14import hashlib
15import json15import json
16+import multiprocessing
16import os17import os
17import re18import re
18import shutil19import shutil
19import subprocess20import subprocess
20import sys21import sys
21import tempfile22import tempfile
23+from concurrent.futures import ProcessPoolExecutor
22from contextlib import contextmanager24from contextlib import contextmanager
23from importlib import import_module25from importlib import import_module
24from importlib.metadata import PackageNotFoundError, distribution26from importlib.metadata import PackageNotFoundError, distribution
25from pathlib import Path27from pathlib import Path
28+from types import SimpleNamespace
26 29 
27 30 
28def digest(path):31def digest(path):
@@ -316,6 +319,77 @@ def validate_aclnn_host(root, aclnn_type, target="dav-3510"):
316 raise ValueError(f"Missing ACLNN module sources: {name}")319 raise ValueError(f"Missing ACLNN module sources: {name}")
317 320 
318 321 
322+def compile_native_shard(source, target, mode, directory, index, count):
323+ from cannbotdsl.aot.collection import collect, discover
324+ from cannbotdsl.core.compiler.builder import lower_to_source
325+ from opkit.api import _compilation_symbols
326+ from opkit.device import DevicePublication
327+ 
328+ device = DevicePublication(directory, target)
329+ device._native_host_abi = mode == "host"
330+ try:
331+ with _compilation_symbols(), discover(collectors=source.parent, pattern=source.name) as tasks:
332+ with lower_to_source(), collect(device):
333+ for name, callback in tasks.items():
334+ device.task = name
335+ callback.__transformer_aot_shard__(index, count)
336+ if mode == "kernel":
337+ for key, record in device.records.items():
338+ device.modules[key] = device._compile(key, record["operator"])
339+ return device.records, device.modules
340+ except Exception as exc:
341+ raise RuntimeError(f"{source.name} {mode} shard {index + 1}/{count} failed") from exc
342+ finally:
343+ device.close()
344+ 
345+ 
346+def build_native_parallel(source, output, target, mode, jobs):
347+ # Use independent processes because CANNBotDSL collection owns its process
348+ # and does not support concurrent threads or forked compiler state.
349+ from opkit.device import DevicePublication
350+ from opkit.package import Package, _publish_native
351+ 
352+ jobs = min(jobs, 32)
353+ print(f"CANNBotDSL {source.stem} {mode}: compiling with {jobs} workers", flush=True)
354+ with tempfile.TemporaryDirectory(prefix=".shards-", dir=output.parent) as scratch:
355+ scratch = Path(scratch)
356+ device = DevicePublication(scratch / "device", target)
357+ device.root.mkdir()
358+ try:
359+ with ProcessPoolExecutor(max_workers=jobs, mp_context=multiprocessing.get_context("spawn")) as pool:
360+ futures = [
361+ pool.submit(compile_native_shard, source, target, mode, scratch / str(index), index, jobs)
362+ for index in range(jobs)
363+ ]
364+ for index, future in enumerate(futures):
365+ records, modules = future.result()
366+ for key, record in records.items():
367+ device.task = record["operator"]
368+ for origin in record["sources"]:
369+ function = SimpleNamespace(__qualname__=origin["function"], __module__=origin["module"])
370+ device.record(record["artifact"], function)
371+ if key in modules and key not in device.modules:
372+ module = modules[key]
373+ for field in ("binary", "metadata"):
374+ relative = module[field]
375+ destination = device.root / relative
376+ destination.parent.mkdir(parents=True, exist_ok=True)
377+ shutil.copy2(scratch / str(index) / relative, destination)
378+ device.modules[key] = module
379+ if not device.records:
380+ raise ValueError(f"{source.name} collected no DSL compile requests")
381+ output.mkdir()
382+ if mode == "host":
383+ _publish_native(output, device, mode)
384+ else:
385+ package = Package(output)
386+ device.publish_all(output)
387+ package.finish(dict(target=target, tasks=sorted(device.tasks), binary_format="device", mode=mode, aclnn={}))
388+ print(f"CANNBotDSL {source.stem} {mode}: published {len(device.records)} profiles", flush=True)
389+ finally:
390+ device.close()
391+ 
392+ 
319def build(393def build(
320 source,394 source,
321 output,395 output,
@@ -325,6 +399,7 @@ def build(
325 inputs=None,399 inputs=None,
326 aclnn_type=None,400 aclnn_type=None,
327 target="dav-3510",401 target="dav-3510",
402+ jobs=1,
328):403):
329 import opkit404 import opkit
330 405 
@@ -350,21 +425,24 @@ def build(
350 l2=aclnn_type == "aclnn",425 l2=aclnn_type == "aclnn",
351 )426 )
352 )427 )
353- opkit.build(428+ if jobs > 1 and aclnn_type is None:
354- collectors=source.parent,429+ build_native_parallel(source, stage, target, mode, jobs)
355- # Only ACLNN Host publication collects the L2 declaration.430+ else:
356- # Native Host and device profiles come from the implementation.431+ opkit.build(
357- pattern=(432+ collectors=source.parent,
358- "*.py"433+ # Only ACLNN Host publication collects the L2 declaration.
359- if source.with_name("aclnn_" + source.name).is_file()434+ # Native Host and device profiles come from the implementation.
360- and aclnn_type is not None435+ pattern=(
361- else source.name436+ "*.py"
362- ),437+ if source.with_name("aclnn_" + source.name).is_file()
363- target=target,438+ and aclnn_type is not None
364- output=stage,439+ else source.name
365- mode=mode,440+ ),
366- **options,441+ target=target,
367- )442+ output=stage,
443+ mode=mode,
444+ **options,
445+ )
368 if aclnn_type is None and mode == "host":446 if aclnn_type is None and mode == "host":
369 extract_host_objects(stage)447 extract_host_objects(stage)
370 if aclnn_type is not None and mode == "host":448 if aclnn_type is not None and mode == "host":
@@ -387,6 +465,7 @@ def main():
387 parser.add_argument("--output", type=Path)465 parser.add_argument("--output", type=Path)
388 parser.add_argument("--soc", default="ascend950")466 parser.add_argument("--soc", default="ascend950")
389 parser.add_argument("--target", default="dav-3510")467 parser.add_argument("--target", default="dav-3510")
468+ parser.add_argument("--jobs", type=int, default=1)
390 parser.add_argument("--kernel-output", type=Path)469 parser.add_argument("--kernel-output", type=Path)
391 parser.add_argument("--packages", nargs="*", type=Path, default=[])470 parser.add_argument("--packages", nargs="*", type=Path, default=[])
392 parser.add_argument("--inputs", type=Path)471 parser.add_argument("--inputs", type=Path)
@@ -395,6 +474,8 @@ def main():
395 "--aclnn-type", choices=("aclnn", "aclnn_inner", "aclnn_exclude")474 "--aclnn-type", choices=("aclnn", "aclnn_inner", "aclnn_exclude")
396 )475 )
397 args = parser.parse_args()476 args = parser.parse_args()
477+ if args.jobs < 1:
478+ parser.error("--jobs must be positive")
398 if args.output is None:479 if args.output is None:
399 parser.error("--output is required for publication modes")480 parser.error("--output is required for publication modes")
400 if args.mode == "inputs":481 if args.mode == "inputs":
@@ -421,6 +502,7 @@ def main():
421 args.inputs,502 args.inputs,
422 args.aclnn_type,503 args.aclnn_type,
423 args.target,504 args.target,
505+ args.jobs,
424 )506 )
425 507 
426 508