From d3c60ba35d10ba0c278259fea93e1ff95a6d9e93 Mon Sep 17 00:00:00 2001 From: Crane-Liu Date: Mon, 27 Jul 2026 21:16:46 +0800 Subject: [PATCH] Fix: bind AICore run streams to code images Compute a stable identity from each callable's AICore child binaries and function mapping, then carry it through callable registration. Reuse a slot's AICPU stream independently, but recreate its AICore stream whenever the loaded image changes. Extend hardware coverage with alternating add/sub images and retain same-image reuse checks. --- python/simpler/task_interface.py | 2 +- python/simpler/worker.py | 10 +-- .../platform/onboard/host/device_runner.cpp | 46 ++++++---- .../platform/onboard/host/device_runner.h | 11 +-- .../host_build_graph/host/runtime_maker.cpp | 3 +- .../host/runtime_maker.cpp | 3 +- .../host_build_graph/host/runtime_maker.cpp | 3 +- .../host/runtime_maker.cpp | 3 +- .../platform/onboard/host/c_api_shared.cpp | 10 +-- .../onboard/host/device_runner_base.cpp | 12 +-- .../onboard/host/device_runner_base.h | 17 ++-- .../task_interface/chip_callable_layout.h | 19 +++- .../task_interface/prepare_callable_common.h | 4 +- src/common/utils/fnv1a_64.h | 14 +-- src/common/worker/chip_worker.h | 8 +- src/common/worker/pto_runtime_c_api.h | 9 +- .../kernels/aiv/kernel_sub.cpp | 64 ++++++++++++++ .../run_stream_reuse/test_run_stream_reuse.py | 86 ++++++++++++++++--- .../test_chip_callable_upload_immutable.cpp | 32 +++++++ 19 files changed, 278 insertions(+), 78 deletions(-) create mode 100644 tests/st/a2a3/host_build_graph/run_stream_reuse/kernels/aiv/kernel_sub.cpp diff --git a/python/simpler/task_interface.py b/python/simpler/task_interface.py index b18142329c..5c20d9bd9e 100644 --- a/python/simpler/task_interface.py +++ b/python/simpler/task_interface.py @@ -1264,7 +1264,7 @@ def host_dlopen_count(self): @property def run_stream_set_create_count(self): - """Number of run stream sets the bound runner has created.""" + """Number of run stream generations the bound runner has created.""" return self._impl.run_stream_set_create_count def malloc(self, size): diff --git a/python/simpler/worker.py b/python/simpler/worker.py index 2c3fa7bb6c..bcb70e9970 100644 --- a/python/simpler/worker.py +++ b/python/simpler/worker.py @@ -6155,12 +6155,12 @@ def host_dlopen_count(self) -> int: @property def run_stream_set_create_count(self) -> int: - """L2 only: number of run stream sets the bound runner has created. + """L2 only: number of run stream generations the runner has created. - A set belongs to a pipeline slot and is reused for every run on that - slot, so a worker that has served any number of runs reports 1. - Returns 0 on non-L2 workers and on platforms whose runs use the - persistent bootstrap stream pair (simulation, a5). + AICPU streams belong to pipeline slots. AICore streams are reused only + while the loaded AICore image is unchanged, so each code transition + advances this count. Returns 0 on non-L2 workers and on platforms whose + runs use the persistent bootstrap stream pair (simulation, a5). """ if self.level != 2 or self._chip_worker is None: return 0 diff --git a/src/a2a3/platform/onboard/host/device_runner.cpp b/src/a2a3/platform/onboard/host/device_runner.cpp index 0ab734f798..5cd2536015 100644 --- a/src/a2a3/platform/onboard/host/device_runner.cpp +++ b/src/a2a3/platform/onboard/host/device_runner.cpp @@ -248,9 +248,16 @@ int DeviceRunner::run(Runtime &runtime, const CallConfig &config) { return rc; } - rc = ensure_run_stream_set(kPipelineSlot); + const int32_t callable_id = runtime.get_active_callable_id(); + auto callable_it = callables_.find(callable_id); + if (callable_it == callables_.end()) { + LOG_ERROR("run() has no registered state for callable_id=%d", callable_id); + return -1; + } + const uint64_t aicore_image_hash = callable_it->second.aicore_image_hash; + rc = ensure_run_stream_set(kPipelineSlot, aicore_image_hash); if (rc != 0) { - LOG_ERROR("ensure_run_stream_set(%u) failed: %d", kPipelineSlot, rc); + LOG_ERROR("ensure_run_stream_set(%u, image=0x%lx) failed: %d", kPipelineSlot, aicore_image_hash, rc); return rc; } @@ -480,19 +487,12 @@ int DeviceRunner::run(Runtime &runtime, const CallConfig &config) { return 0; } -int DeviceRunner::ensure_run_stream_set(unsigned slot) { +int DeviceRunner::ensure_run_stream_set(unsigned slot, uint64_t aicore_image_hash) { if (slot >= kRunStreamSetCount) { LOG_ERROR("ensure_run_stream_set: invalid slot %u", slot); return -1; } RunStreamSet &streams = run_stream_sets_[slot]; - if (streams.aicpu != nullptr && streams.aicore != nullptr) { - return 0; - } - - // Reached only on the first run for this slot, or after a partial create - // rolled one stream back; rtStreamCreate costs ~300 us and the set carries - // no per-run content, so it must not be rebuilt per run. if (streams.aicpu == nullptr) { int rc = rtStreamCreate(&streams.aicpu, 0); if (rc != 0) { @@ -501,16 +501,31 @@ int DeviceRunner::ensure_run_stream_set(unsigned slot) { return rc; } } - if (streams.aicore == nullptr) { - int rc = rtStreamCreate(&streams.aicore, 0); + if (streams.aicore != nullptr && streams.has_aicore_image && streams.aicore_image_hash == aicore_image_hash) { + return 0; + } + + if (streams.aicore != nullptr) { + int rc = rtStreamDestroy(streams.aicore); if (rc != 0) { - LOG_ERROR("rtStreamCreate (run AICore slot %u) failed: %d", slot, rc); - ACL_LOG_ERROR_DETAIL(rc); + LOG_ERROR("rtStreamDestroy (retired AICore slot %u) failed: %d", slot, rc); return rc; } + streams.aicore = nullptr; + streams.has_aicore_image = false; + } + + int rc = rtStreamCreate(&streams.aicore, 0); + if (rc != 0) { + LOG_ERROR("rtStreamCreate (run AICore slot %u) failed: %d", slot, rc); + ACL_LOG_ERROR_DETAIL(rc); + streams.aicore = nullptr; + return rc; } + streams.aicore_image_hash = aicore_image_hash; + streams.has_aicore_image = true; ++run_stream_sets_created_; - LOG_INFO_V0("DeviceRunner: run stream set %u created", slot); + LOG_INFO_V0("DeviceRunner: run stream generation %zu created for slot %u", run_stream_sets_created_, slot); return 0; } @@ -539,6 +554,7 @@ int DeviceRunner::destroy_run_stream_sets() { } capture(destroy_rc); streams.aicore = nullptr; + streams.has_aicore_image = false; } } return rc; diff --git a/src/a2a3/platform/onboard/host/device_runner.h b/src/a2a3/platform/onboard/host/device_runner.h index be258712b0..cc5544415f 100644 --- a/src/a2a3/platform/onboard/host/device_runner.h +++ b/src/a2a3/platform/onboard/host/device_runner.h @@ -228,17 +228,18 @@ class DeviceRunner : public DeviceRunnerBase { struct RunStreamSet { rtStream_t aicpu{nullptr}; rtStream_t aicore{nullptr}; + uint64_t aicore_image_hash{0}; + bool has_aicore_image{false}; }; // One slot per in-flight run the pipeline contract can declare. static constexpr unsigned kRunStreamSetCount = PTO_PIPELINE_MAX_DEPTH; - // A stream set carries no per-run content, so it is created on first use - // and reused for every later run on this slot; `destroy_run_stream_sets()` - // in finalize() is the sole release point, as for the persistent bootstrap - // pair. Only slot 0 is selected while the contract is K=1. + // AICPU streams belong to slots. AICore streams additionally belong to the + // loaded code image because the platform has no explicit instruction-cache + // invalidation operation for code replaced in GM. RunStreamSet run_stream_sets_[kRunStreamSetCount]{}; size_t run_stream_sets_created_{0}; - int ensure_run_stream_set(unsigned slot); + int ensure_run_stream_set(unsigned slot, uint64_t aicore_image_hash); int destroy_run_stream_sets(); // The kernel submission boundary is separate from the stream wait and the diff --git a/src/a2a3/runtime/host_build_graph/host/runtime_maker.cpp b/src/a2a3/runtime/host_build_graph/host/runtime_maker.cpp index f8de55082c..80cc399a0c 100644 --- a/src/a2a3/runtime/host_build_graph/host/runtime_maker.cpp +++ b/src/a2a3/runtime/host_build_graph/host/runtime_maker.cpp @@ -558,7 +558,8 @@ register_callable_impl(const ChipCallable *callable, uint64_t (*upload_fn)(const LOG_INFO_V0("Registering %d kernel(s) in register_callable_impl", callable->child_count()); if (upload_and_collect_child_addrs( - callable, upload_fn, &out->kernel_addrs, &out->chip_buffer_dev, &out->chip_buffer_hash + callable, upload_fn, &out->kernel_addrs, &out->chip_buffer_dev, &out->chip_buffer_hash, + &out->aicore_image_hash ) != 0) { LOG_ERROR("Failed to upload ChipCallable buffer"); return -1; diff --git a/src/a2a3/runtime/tensormap_and_ringbuffer/host/runtime_maker.cpp b/src/a2a3/runtime/tensormap_and_ringbuffer/host/runtime_maker.cpp index c7ce87c6c4..9bb1b611cd 100644 --- a/src/a2a3/runtime/tensormap_and_ringbuffer/host/runtime_maker.cpp +++ b/src/a2a3/runtime/tensormap_and_ringbuffer/host/runtime_maker.cpp @@ -416,7 +416,8 @@ register_callable_impl(const ChipCallable *callable, uint64_t (*upload_fn)(const LOG_INFO_V0("Registering %d kernel(s) in register_callable_impl", callable->child_count()); if (upload_and_collect_child_addrs( - callable, upload_fn, &out->kernel_addrs, &out->chip_buffer_dev, &out->chip_buffer_hash + callable, upload_fn, &out->kernel_addrs, &out->chip_buffer_dev, &out->chip_buffer_hash, + &out->aicore_image_hash ) != 0) { LOG_ERROR("Failed to upload ChipCallable buffer"); return -1; diff --git a/src/a5/runtime/host_build_graph/host/runtime_maker.cpp b/src/a5/runtime/host_build_graph/host/runtime_maker.cpp index e3b55486aa..ff55ae60ad 100644 --- a/src/a5/runtime/host_build_graph/host/runtime_maker.cpp +++ b/src/a5/runtime/host_build_graph/host/runtime_maker.cpp @@ -322,7 +322,8 @@ int register_callable_impl(const ChipCallable *callable, uint64_t (*upload_fn)(c LOG_INFO_V0("Registering %d kernel(s) in register_callable_impl", callable->child_count()); if (upload_and_collect_child_addrs( - callable, upload_fn, &out->kernel_addrs, &out->chip_buffer_dev, &out->chip_buffer_hash + callable, upload_fn, &out->kernel_addrs, &out->chip_buffer_dev, &out->chip_buffer_hash, + &out->aicore_image_hash ) != 0) { LOG_ERROR("Failed to upload ChipCallable buffer"); return -1; diff --git a/src/a5/runtime/tensormap_and_ringbuffer/host/runtime_maker.cpp b/src/a5/runtime/tensormap_and_ringbuffer/host/runtime_maker.cpp index 187a66d91f..07754e0693 100644 --- a/src/a5/runtime/tensormap_and_ringbuffer/host/runtime_maker.cpp +++ b/src/a5/runtime/tensormap_and_ringbuffer/host/runtime_maker.cpp @@ -393,7 +393,8 @@ register_callable_impl(const ChipCallable *callable, uint64_t (*upload_fn)(const LOG_INFO_V0("Registering %d kernel(s) in register_callable_impl", callable->child_count()); if (upload_and_collect_child_addrs( - callable, upload_fn, &out->kernel_addrs, &out->chip_buffer_dev, &out->chip_buffer_hash + callable, upload_fn, &out->kernel_addrs, &out->chip_buffer_dev, &out->chip_buffer_hash, + &out->aicore_image_hash ) != 0) { LOG_ERROR("Failed to upload ChipCallable buffer"); return -1; diff --git a/src/common/platform/onboard/host/c_api_shared.cpp b/src/common/platform/onboard/host/c_api_shared.cpp index bd8c668e95..03e3f0934a 100644 --- a/src/common/platform/onboard/host/c_api_shared.cpp +++ b/src/common/platform/onboard/host/c_api_shared.cpp @@ -434,17 +434,17 @@ int simpler_register_callable(DeviceContextHandle ctx, int32_t callable_id, cons bool needs_aicpu_register = false; if (artifacts.host_dlopen_handle != nullptr) { rc = runner->record_host_orch_callable( - callable_id, artifacts.chip_buffer_hash, artifacts.host_dlopen_handle, artifacts.host_orch_func_ptr, - std::move(kernel_addrs), std::move(artifacts.signature) + callable_id, artifacts.chip_buffer_hash, artifacts.aicore_image_hash, artifacts.host_dlopen_handle, + artifacts.host_orch_func_ptr, std::move(kernel_addrs), std::move(artifacts.signature) ); if (rc != 0) return rc; host_dlopen_guard.dismiss(); chip_buffer_guard.dismiss(); } else { rc = runner->record_device_orch_callable( - callable_id, artifacts.chip_buffer_hash, artifacts.chip_buffer_dev, artifacts.orch_so_data, - artifacts.orch_so_size, artifacts.func_name.c_str(), artifacts.config_name.c_str(), - std::move(kernel_addrs), std::move(artifacts.signature) + callable_id, artifacts.chip_buffer_hash, artifacts.aicore_image_hash, artifacts.chip_buffer_dev, + artifacts.orch_so_data, artifacts.orch_so_size, artifacts.func_name.c_str(), + artifacts.config_name.c_str(), std::move(kernel_addrs), std::move(artifacts.signature) ); if (rc != 0) return rc; chip_buffer_guard.dismiss(); diff --git a/src/common/platform/onboard/host/device_runner_base.cpp b/src/common/platform/onboard/host/device_runner_base.cpp index 0af3d711c2..30c2bcd240 100644 --- a/src/common/platform/onboard/host/device_runner_base.cpp +++ b/src/common/platform/onboard/host/device_runner_base.cpp @@ -735,9 +735,9 @@ int DeviceRunnerBase::launch_device_register(int32_t callable_id) { } int DeviceRunnerBase::record_device_orch_callable( - int32_t callable_id, uint64_t chip_buffer_hash, uint64_t chip_dev, const void *orch_so_data, size_t orch_so_size, - const char *func_name, const char *config_name, std::vector> kernel_addrs, - std::vector signature + int32_t callable_id, uint64_t chip_buffer_hash, uint64_t aicore_image_hash, uint64_t chip_dev, + const void *orch_so_data, size_t orch_so_size, const char *func_name, const char *config_name, + std::vector> kernel_addrs, std::vector signature ) { // The AICPU executor reserves `orch_so_table_[MAX_REGISTERED_CALLABLE_IDS]` // (declared in src/common/task_interface/callable_protocol.h) and indexes @@ -767,6 +767,7 @@ int DeviceRunnerBase::record_device_orch_callable( CallableState state; state.hash = hash; state.chip_buffer_hash = chip_buffer_hash; + state.aicore_image_hash = aicore_image_hash; state.dev_orch_so_addr = chip_dev + offsetof(ChipCallable, storage_); state.dev_orch_so_size = orch_so_size; state.func_name = (func_name != nullptr) ? func_name : ""; @@ -782,8 +783,8 @@ int DeviceRunnerBase::record_device_orch_callable( } int DeviceRunnerBase::record_host_orch_callable( - int32_t callable_id, uint64_t chip_buffer_hash, void *host_dlopen_handle, void *host_orch_func_ptr, - std::vector> kernel_addrs, std::vector signature + int32_t callable_id, uint64_t chip_buffer_hash, uint64_t aicore_image_hash, void *host_dlopen_handle, + void *host_orch_func_ptr, std::vector> kernel_addrs, std::vector signature ) { if (callable_id < 0 || callable_id >= MAX_REGISTERED_CALLABLE_IDS) { LOG_ERROR( @@ -806,6 +807,7 @@ int DeviceRunnerBase::record_host_orch_callable( CallableState state; state.chip_buffer_hash = chip_buffer_hash; + state.aicore_image_hash = aicore_image_hash; state.host_dlopen_handle = host_dlopen_handle; state.host_orch_func_ptr = host_orch_func_ptr; state.kernel_addrs = std::move(kernel_addrs); diff --git a/src/common/platform/onboard/host/device_runner_base.h b/src/common/platform/onboard/host/device_runner_base.h index b4101ec957..4953ee8d84 100644 --- a/src/common/platform/onboard/host/device_runner_base.h +++ b/src/common/platform/onboard/host/device_runner_base.h @@ -303,8 +303,8 @@ class DeviceRunnerBase { * @return 0 on success, negative on failure. */ int record_device_orch_callable( - int32_t callable_id, uint64_t chip_buffer_hash, uint64_t chip_dev, const void *orch_so_data, - size_t orch_so_size, const char *func_name, const char *config_name, + int32_t callable_id, uint64_t chip_buffer_hash, uint64_t aicore_image_hash, uint64_t chip_dev, + const void *orch_so_data, size_t orch_so_size, const char *func_name, const char *config_name, std::vector> kernel_addrs, std::vector signature ); @@ -319,8 +319,9 @@ class DeviceRunnerBase { * dlclose'd by `unregister_callable`. Increments `host_dlopen_total_`. */ int record_host_orch_callable( - int32_t callable_id, uint64_t chip_buffer_hash, void *host_dlopen_handle, void *host_orch_func_ptr, - std::vector> kernel_addrs, std::vector signature + int32_t callable_id, uint64_t chip_buffer_hash, uint64_t aicore_image_hash, void *host_dlopen_handle, + void *host_orch_func_ptr, std::vector> kernel_addrs, + std::vector signature ); /** @@ -420,10 +421,9 @@ class DeviceRunnerBase { size_t host_dlopen_count() const { return host_dlopen_total_; } /** - * Number of run stream sets this runner has created. A set belongs to a - * pipeline slot and is reused for every run on that slot, so a runner that - * has served any number of runs on one slot reports 1. Arches whose runs - * use the persistent pair report 0. + * Number of run stream generations this runner has created. AICPU streams + * belong to pipeline slots, while an AICore stream is reused only for the + * same AICore image. Arches whose runs use the persistent pair report 0. */ virtual size_t run_stream_set_create_count() const { return 0; } @@ -806,6 +806,7 @@ class DeviceRunnerBase { // chip_buffer_hash, which keys the retained buffer. uint64_t hash{0}; uint64_t chip_buffer_hash{0}; + uint64_t aicore_image_hash{0}; uint64_t dev_orch_so_addr{0}; size_t dev_orch_so_size{0}; std::string func_name; diff --git a/src/common/task_interface/chip_callable_layout.h b/src/common/task_interface/chip_callable_layout.h index a0d338a59b..e19f945592 100644 --- a/src/common/task_interface/chip_callable_layout.h +++ b/src/common/task_interface/chip_callable_layout.h @@ -30,9 +30,10 @@ #include "utils/fnv1a_64.h" struct ChipCallableLayout { - size_t header_size; // offsetof(ChipCallable, storage_) - size_t total_size; // header_size + storage_used (matches make_callable()) - uint64_t content_hash; // FNV-1a 64 over [callable, total_size) + size_t header_size; // offsetof(ChipCallable, storage_) + size_t total_size; // header_size + storage_used (matches make_callable()) + uint64_t content_hash; // FNV-1a 64 over [callable, total_size) + uint64_t aicore_image_hash; // FNV-1a 64 over func ids and child binaries }; /** @@ -45,15 +46,25 @@ struct ChipCallableLayout { inline ChipCallableLayout compute_chip_callable_layout(const ChipCallable *callable) { constexpr size_t kHeaderSize = offsetof(ChipCallable, storage_); size_t storage_used = static_cast(callable->binary_size()); + const int32_t child_count = callable->child_count(); + uint64_t aicore_image_hash = simpler::common::utils::fnv1a_64(&child_count, sizeof(child_count)); for (int32_t i = 0; i < callable->child_count(); ++i) { const CoreCallable &c = callable->child(i); + const int32_t func_id = callable->child_func_id(i); + const uint32_t binary_size = c.binary_size(); + aicore_image_hash = simpler::common::utils::fnv1a_64_append(aicore_image_hash, &func_id, sizeof(func_id)); + aicore_image_hash = + simpler::common::utils::fnv1a_64_append(aicore_image_hash, &binary_size, sizeof(binary_size)); + aicore_image_hash = simpler::common::utils::fnv1a_64_append( + aicore_image_hash, c.binary_data(), static_cast(binary_size) + ); size_t child_total = CoreCallable::binary_data_offset() + static_cast(c.binary_size()); size_t end = static_cast(callable->child_offset(i)) + child_total; if (end > storage_used) storage_used = end; } const size_t total_size = kHeaderSize + storage_used; const uint64_t hash = simpler::common::utils::fnv1a_64(reinterpret_cast(callable), total_size); - return ChipCallableLayout{kHeaderSize, total_size, hash}; + return ChipCallableLayout{kHeaderSize, total_size, hash, aicore_image_hash}; } /** diff --git a/src/common/task_interface/prepare_callable_common.h b/src/common/task_interface/prepare_callable_common.h index d1e428255e..5c760ecb2c 100644 --- a/src/common/task_interface/prepare_callable_common.h +++ b/src/common/task_interface/prepare_callable_common.h @@ -65,6 +65,7 @@ struct CallableArtifacts { void *host_dlopen_handle{nullptr}; // hbg only void *host_orch_func_ptr{nullptr}; // hbg only uint64_t chip_buffer_hash{0}; // FNV-1a hash for the whole ChipCallable buffer + uint64_t aicore_image_hash{0}; // FNV-1a hash for func ids and AICore child binaries uint64_t chip_buffer_dev{0}; // device address of the ChipCallable header const void *orch_so_data{nullptr}; // trb only; host view used for validation/hash only size_t orch_so_size{0}; // trb only @@ -90,7 +91,7 @@ struct CallableArtifacts { */ inline int upload_and_collect_child_addrs( const ChipCallable *callable, uint64_t (*upload_fn)(const void *), std::vector *out, - uint64_t *out_chip_dev = nullptr, uint64_t *out_chip_hash = nullptr + uint64_t *out_chip_dev = nullptr, uint64_t *out_chip_hash = nullptr, uint64_t *out_aicore_image_hash = nullptr ) { if (callable == nullptr || upload_fn == nullptr || out == nullptr) return -1; out->clear(); @@ -100,6 +101,7 @@ inline int upload_and_collect_child_addrs( if (chip_dev == 0) return -1; if (out_chip_dev != nullptr) *out_chip_dev = chip_dev; if (out_chip_hash != nullptr) *out_chip_hash = layout.content_hash; + if (out_aicore_image_hash != nullptr) *out_aicore_image_hash = layout.aicore_image_hash; out->reserve(static_cast(callable->child_count())); for (int32_t i = 0; i < callable->child_count(); ++i) { diff --git a/src/common/utils/fnv1a_64.h b/src/common/utils/fnv1a_64.h index b9b6763408..7be0e71724 100644 --- a/src/common/utils/fnv1a_64.h +++ b/src/common/utils/fnv1a_64.h @@ -20,15 +20,19 @@ namespace simpler::common::utils { // FNV-1a 64-bit content hash. Deterministic, allocation-free, ~µs / MB. // Used as a generic content-keyed dedup key (ChipCallable buffer hashing in // DeviceRunner, ELF Build-ID fallback in elf_build_id.h, etc.). -inline uint64_t fnv1a_64(const void *data, std::size_t len) { +inline uint64_t fnv1a_64_append(uint64_t hash, const void *data, std::size_t len) { constexpr uint64_t kPrime = 0x00000100000001b3ULL; - uint64_t h = 0xcbf29ce484222325ULL; const auto *p = static_cast(data); for (std::size_t i = 0; i < len; ++i) { - h ^= p[i]; - h *= kPrime; + hash ^= p[i]; + hash *= kPrime; } - return h; + return hash; +} + +inline uint64_t fnv1a_64(const void *data, std::size_t len) { + constexpr uint64_t kOffsetBasis = 0xcbf29ce484222325ULL; + return fnv1a_64_append(kOffsetBasis, data, len); } } // namespace simpler::common::utils diff --git a/src/common/worker/chip_worker.h b/src/common/worker/chip_worker.h index 419137ea4c..7ead68ff33 100644 --- a/src/common/worker/chip_worker.h +++ b/src/common/worker/chip_worker.h @@ -90,11 +90,9 @@ class ChipWorker { /// `aicpu_dlopen_count` for the trb path; returns 0 on device-orch variants. size_t host_dlopen_count() const; - /// Number of run stream sets the bound runner has created. A set belongs - /// to a pipeline slot and is reused for every run on that slot, so a - /// runner that has served any number of runs on one slot reports 1; - /// platforms whose runs use the persistent bootstrap pair report 0. Used - /// by tests to assert that repeated runs do not rebuild the set per run. + /// Number of run stream generations the bound runner has created. AICPU + /// streams belong to slots; AICore streams are reused only while the loaded + /// code image is unchanged. Platforms using the persistent pair report 0. size_t run_stream_set_create_count() const; uint64_t malloc(size_t size); diff --git a/src/common/worker/pto_runtime_c_api.h b/src/common/worker/pto_runtime_c_api.h index d137733985..52147053bf 100644 --- a/src/common/worker/pto_runtime_c_api.h +++ b/src/common/worker/pto_runtime_c_api.h @@ -307,11 +307,10 @@ size_t get_aicpu_dlopen_count(DeviceContextHandle ctx); size_t get_host_dlopen_count(DeviceContextHandle ctx); /** - * Number of run stream sets the runner bound to `ctx` has created. A set - * belongs to a pipeline slot and is reused for every run on that slot, so a - * runner that has served any number of runs on one slot reports 1. Returns 0 - * on platforms whose runs use the persistent bootstrap pair. Used by tests to - * assert that repeated `simpler_run` calls do not rebuild the set per run. + * Number of run stream generations the runner bound to `ctx` has created. + * AICPU streams belong to pipeline slots; AICore streams are reused only while + * their loaded code image is unchanged. Returns 0 on platforms whose runs use + * the persistent bootstrap pair. */ size_t get_run_stream_set_create_count(DeviceContextHandle ctx); diff --git a/tests/st/a2a3/host_build_graph/run_stream_reuse/kernels/aiv/kernel_sub.cpp b/tests/st/a2a3/host_build_graph/run_stream_reuse/kernels/aiv/kernel_sub.cpp new file mode 100644 index 0000000000..2226f5b815 --- /dev/null +++ b/tests/st/a2a3/host_build_graph/run_stream_reuse/kernels/aiv/kernel_sub.cpp @@ -0,0 +1,64 @@ +/* + * Copyright (c) PyPTO Contributors. + * This program is free software, you can redistribute it and/or modify it under the terms and conditions of + * CANN Open Software License Agreement Version 2.0 (the "License"). + * Please refer to the License for details. You may not use this file except in compliance with the License. + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. + * See LICENSE in the root of the software repository for the full text of the License. + * ----------------------------------------------------------------------------------------------------------- + */ + +#include +#include + +#include "tensor.h" + +using namespace pto; + +#include "pipe_sync.h" + +#ifndef __gm__ +#define __gm__ +#endif + +#ifndef __aicore__ +#define __aicore__ [aicore] +#endif + +extern "C" __aicore__ __attribute__((always_inline)) void kernel_entry(__gm__ int64_t *args) { + __gm__ Tensor *src0_tensor = reinterpret_cast<__gm__ Tensor *>(args[0]); + __gm__ Tensor *src1_tensor = reinterpret_cast<__gm__ Tensor *>(args[1]); + __gm__ Tensor *out_tensor = reinterpret_cast<__gm__ Tensor *>(args[2]); + + __gm__ float *src0 = reinterpret_cast<__gm__ float *>(src0_tensor->buffer.addr) + src0_tensor->start_offset; + __gm__ float *src1 = reinterpret_cast<__gm__ float *>(src1_tensor->buffer.addr) + src1_tensor->start_offset; + __gm__ float *out = reinterpret_cast<__gm__ float *>(out_tensor->buffer.addr) + out_tensor->start_offset; + + constexpr int kRows = 128; + constexpr int kCols = 128; + using Shape5D = Shape<1, 1, 1, kRows, kCols>; + using Stride5D = Stride<1, 1, 1, kCols, 1>; + using GlobalData = GlobalTensor; + using TileData = Tile; + + TileData src0_tile(kRows, kCols); + TileData src1_tile(kRows, kCols); + TileData dst_tile(kRows, kCols); + TASSIGN(src0_tile, 0x0); + TASSIGN(src1_tile, 0x10000); + TASSIGN(dst_tile, 0x20000); + + GlobalData src0_global(src0); + GlobalData src1_global(src1); + GlobalData dst_global(out); + TLOAD(src0_tile, src0_global); + TLOAD(src1_tile, src1_global); + set_flag(PIPE_MTE2, PIPE_V, EVENT_ID0); + wait_flag(PIPE_MTE2, PIPE_V, EVENT_ID0); + TSUB(dst_tile, src0_tile, src1_tile); + set_flag(PIPE_V, PIPE_MTE3, EVENT_ID0); + wait_flag(PIPE_V, PIPE_MTE3, EVENT_ID0); + TSTORE(dst_global, dst_tile); + pipe_sync(); +} diff --git a/tests/st/a2a3/host_build_graph/run_stream_reuse/test_run_stream_reuse.py b/tests/st/a2a3/host_build_graph/run_stream_reuse/test_run_stream_reuse.py index 8e0444ca4a..049927cdf3 100644 --- a/tests/st/a2a3/host_build_graph/run_stream_reuse/test_run_stream_reuse.py +++ b/tests/st/a2a3/host_build_graph/run_stream_reuse/test_run_stream_reuse.py @@ -7,16 +7,16 @@ # INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. # See LICENSE in the root of the software repository for the full text of the License. # ----------------------------------------------------------------------------------------------------------- -"""A2A3 run stream sets belong to a pipeline slot, not to a run. +"""A2A3 run streams are reused only while their code image is unchanged. A2A3 submits each run's AICore and AICPU kernels on the stream set of the -selected pipeline slot rather than on the persistent bootstrap pair. A set -carries no per-run content, so it is created on first use and reused; rebuilding -it per run costs two rtStreamCreate plus two rtStreamDestroy (~1.2 ms) on the -synchronous host path around KernelLaunch and buys nothing. +selected pipeline slot rather than on the persistent bootstrap pair. The AICPU +stream belongs to the slot, while the AICore stream is also bound to the loaded +code image. Reusing it after different code replaces the image can execute stale +instructions because the platform has no explicit AICore I-cache invalidation. `Worker.run_stream_set_create_count` reports how many sets the bound runner has -built, which is what makes that invariant assertable from here. +built, including every fresh AICore stream created for a code transition. """ import pytest @@ -24,11 +24,43 @@ from simpler.task_interface import ArgDirection as D from simpler_setup import SceneTestCase, TaskArgsBuilder, Tensor, scene_test +from simpler_setup.scene_test import _build_chip_task_args, _compare_outputs _VECTOR_KERNELS = "../vector_example/kernels" _REPEATED_RUNS = 4 +@scene_test(level=2, runtime="host_build_graph") +class _SubtractCallable(SceneTestCase): + CALLABLE = { + "orchestration": { + "source": f"{_VECTOR_KERNELS}/orchestration/example_orch.cpp", + "function_name": "aicpu_orchestration_entry", + "signature": [D.IN, D.IN, D.OUT], + }, + "incores": [ + { + "func_id": 0, + "source": "kernels/aiv/kernel_sub.cpp", + "core_type": "aiv", + "signature": [D.IN, D.IN, D.OUT], + }, + { + "func_id": 1, + "source": f"{_VECTOR_KERNELS}/aiv/kernel_add_scalar.cpp", + "core_type": "aiv", + "signature": [D.IN, D.OUT], + }, + { + "func_id": 2, + "source": f"{_VECTOR_KERNELS}/aiv/kernel_mul.cpp", + "core_type": "aiv", + "signature": [D.IN, D.IN, D.OUT], + }, + ], + } + + @scene_test(level=2, runtime="host_build_graph") class TestRunStreamReuseHbg(SceneTestCase): """Repeated runs on one worker must share a single run stream set.""" @@ -91,9 +123,43 @@ def test_one_stream_set_serves_repeated_runs(self, st_platform, st_worker): pytest.skip("run stream sets are an a2a3 onboard resource") callable_obj = self.build_callable(st_platform) - self._run_and_validate_l2(st_worker, callable_obj, self.CASES[0], rounds=_REPEATED_RUNS) + self._run_and_validate_l2(st_worker, callable_obj, self.CASES[0], rounds=1) + after_first = st_worker.run_stream_set_create_count + self._run_and_validate_l2(st_worker, callable_obj, self.CASES[0], rounds=_REPEATED_RUNS - 1) - assert st_worker.run_stream_set_create_count == 1, ( - f"expected 1 run stream set for {_REPEATED_RUNS} runs, got " - f"{st_worker.run_stream_set_create_count} — the set is being rebuilt per run" + assert st_worker.run_stream_set_create_count == after_first, ( + f"same-image runs advanced stream generation after the first run: " + f"{after_first} -> {st_worker.run_stream_set_create_count}" ) + + def _run_registered(self, worker, handle, *, subtract): + params = self.CASES[0]["params"] + test_args = self.generate_args(params) + chip_args, output_names = _build_chip_task_args(test_args, self.CALLABLE["orchestration"]["signature"]) + golden_args = test_args.clone() + a, b = golden_args.a, golden_args.b + base = a - b if subtract else a + b + golden_args.f[:] = (base + 1) * (base + 2) + worker.run(handle, chip_args, config=self._build_config(self.CASES[0]["config"])) + _compare_outputs(test_args, golden_args, output_names, self.RTOL, self.ATOL) + + def test_aicore_stream_tracks_code_image(self, st_platform, st_worker): + if st_platform != "a2a3": + pytest.skip("AICore stream code generations are an a2a3 onboard resource") + + add_handle = st_worker.register(self.build_callable(st_platform)) + sub_handle = st_worker.register(_SubtractCallable.compile_chip_callable(st_platform)) + try: + self._run_registered(st_worker, add_handle, subtract=False) + after_add = st_worker.run_stream_set_create_count + + self._run_registered(st_worker, add_handle, subtract=False) + assert st_worker.run_stream_set_create_count == after_add + + for handle, subtract in ((sub_handle, True), (add_handle, False), (sub_handle, True)): + before_transition = st_worker.run_stream_set_create_count + self._run_registered(st_worker, handle, subtract=subtract) + assert st_worker.run_stream_set_create_count == before_transition + 1 + finally: + st_worker.unregister(sub_handle) + st_worker.unregister(add_handle) diff --git a/tests/ut/cpp/types/test_chip_callable_upload_immutable.cpp b/tests/ut/cpp/types/test_chip_callable_upload_immutable.cpp index 1ef5301c85..7bd6b63bde 100644 --- a/tests/ut/cpp/types/test_chip_callable_upload_immutable.cpp +++ b/tests/ut/cpp/types/test_chip_callable_upload_immutable.cpp @@ -36,6 +36,7 @@ #include #include "callable.h" +#include "chip_callable_layout.h" #include "utils/fnv1a_64.h" namespace { @@ -135,3 +136,34 @@ TEST(ChipCallableUploadImmutable, ChildOffsetsAreCallableAligned) { << " is not a multiple of CALLABLE_ALIGN=" << CALLABLE_ALIGN; } } + +TEST(ChipCallableImageHash, IgnoresOrchestrationBytes) { + auto first = build_test_chip_callable(); + auto second = first; + auto *second_callable = reinterpret_cast(second.data()); + ASSERT_GT(second_callable->binary_size(), 0u); + second_callable->storage_[0] ^= 0xff; + + const auto first_layout = compute_chip_callable_layout(reinterpret_cast(first.data())); + const auto second_layout = compute_chip_callable_layout(second_callable); + EXPECT_NE(first_layout.content_hash, second_layout.content_hash); + EXPECT_EQ(first_layout.aicore_image_hash, second_layout.aicore_image_hash); +} + +TEST(ChipCallableImageHash, ChangesWithKernelBytesOrFunctionMapping) { + auto original = build_test_chip_callable(); + auto changed_binary = original; + auto *binary_callable = reinterpret_cast(changed_binary.data()); + auto *binary_child = reinterpret_cast(binary_callable) + offsetof(ChipCallable, storage_) + + binary_callable->child_offset(0) + CoreCallable::binary_data_offset(); + *binary_child ^= 0xff; + + auto changed_mapping = original; + auto *mapping_callable = reinterpret_cast(changed_mapping.data()); + mapping_callable->child_func_ids_[0] += 1; + + const auto original_hash = + compute_chip_callable_layout(reinterpret_cast(original.data())).aicore_image_hash; + EXPECT_NE(original_hash, compute_chip_callable_layout(binary_callable).aicore_image_hash); + EXPECT_NE(original_hash, compute_chip_callable_layout(mapping_callable).aicore_image_hash); +}