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 1a97b547d..15daaad1c 100644 --- a/src/a2a3/runtime/host_build_graph/host/runtime_maker.cpp +++ b/src/a2a3/runtime/host_build_graph/host/runtime_maker.cpp @@ -56,12 +56,31 @@ #include "../runtime/runtime.h" #include "../../../../common/runtime_status/error_log.h" #include "../../../../common/task_interface/call_config.h" +#include "../../../../common/worker/pto_runtime_c_api.h" #include "callable.h" #include "common/platform_config.h" #include "common/unified_log.h" #include "utils/device_arena.h" #include "prepare_callable_common.h" +extern "C" const PipelineContract *get_pipeline_contract(void) { + // Host orchestration materializes this run's own graph into the image it + // uploads, so every device-resident region carries per-run content. + static const PipelineContract contract = { + PTO_PIPELINE_CONTRACT_ABI_VERSION, + 5, + 1, + { + {PTO_PIPELINE_GM_HEAP, PTO_PIPELINE_HOST_PER_RUN, 0}, + {PTO_PIPELINE_GM_SM, PTO_PIPELINE_HOST_PER_RUN, 0}, + {PTO_PIPELINE_RUNTIME_IMAGE, PTO_PIPELINE_HOST_PER_RUN, 0}, + {PTO_PIPELINE_AICPU_STREAM, PTO_PIPELINE_EXEC_HANDLE, 0}, + {PTO_PIPELINE_AICORE_STREAM, PTO_PIPELINE_EXEC_HANDLE, 0}, + }, + }; + return &contract; +} + // RuntimeEnv (call_config.h) is the cross-runtime ABI for per-ring config and // carries RUNTIME_ENV_RING_COUNT slots, shared with tensormap_and_ringbuffer. // host_build_graph is single-ring (PTO2_MAX_RING_DEPTH == 1) and reads only the 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 c3f009278..292c2afbd 100644 --- a/src/a2a3/runtime/tensormap_and_ringbuffer/host/runtime_maker.cpp +++ b/src/a2a3/runtime/tensormap_and_ringbuffer/host/runtime_maker.cpp @@ -47,6 +47,7 @@ #include "../runtime/runtime.h" #include "../../../../common/runtime_status/error_log.h" #include "../../../../common/task_interface/call_config.h" +#include "../../../../common/worker/pto_runtime_c_api.h" #include "callable.h" #include "common/platform_config.h" #include "common/strace.h" @@ -57,6 +58,28 @@ #include "utils/device_arena.h" #include "prepare_callable_common.h" +extern "C" const PipelineContract *get_pipeline_contract(void) { + // Orchestration runs on the device, so this run's own content is confined to + // its task args. The three pooled regions are built and uploaded once per + // callable sizing and then served from the prebuilt-arena cache + // (build_and_cache_prebuilt_arena runs only on a miss), so a run reuses the + // instance the previous run left behind. + static const PipelineContract contract = { + PTO_PIPELINE_CONTRACT_ABI_VERSION, + 6, + 1, + { + {PTO_PIPELINE_TASK_ARGS, PTO_PIPELINE_HOST_PER_RUN, 0}, + {PTO_PIPELINE_GM_HEAP, PTO_PIPELINE_DEVICE_SCRATCH, 0}, + {PTO_PIPELINE_GM_SM, PTO_PIPELINE_DEVICE_SCRATCH, 0}, + {PTO_PIPELINE_RUNTIME_IMAGE, PTO_PIPELINE_DEVICE_SCRATCH, 0}, + {PTO_PIPELINE_AICPU_STREAM, PTO_PIPELINE_EXEC_HANDLE, 0}, + {PTO_PIPELINE_AICORE_STREAM, PTO_PIPELINE_EXEC_HANDLE, 0}, + }, + }; + return &contract; +} + static_assert( RUNTIME_ENV_RING_COUNT == PTO2_MAX_RING_DEPTH, "RuntimeEnv ring count must match PTO2 runtime ring depth" ); diff --git a/src/common/worker/chip_worker.cpp b/src/common/worker/chip_worker.cpp index 567eb43d6..251d5bd5b 100644 --- a/src/common/worker/chip_worker.cpp +++ b/src/common/worker/chip_worker.cpp @@ -11,6 +11,8 @@ #include "chip_worker.h" +#include "pipeline_contract.h" + #include #include @@ -37,6 +39,14 @@ T load_symbol(void *handle, const char *name) { return reinterpret_cast(sym); } +template +T load_optional_symbol(void *handle, const char *name) { + dlerror(); + void *sym = dlsym(handle, name); + if (dlerror() != nullptr) return nullptr; + return reinterpret_cast(sym); +} + std::vector read_binary_file(const std::string &path) { std::ifstream f(path, std::ios::binary | std::ios::ate); if (!f) { @@ -71,6 +81,7 @@ void ChipWorker::init( if (device_id < 0) { throw std::runtime_error("ChipWorker::init requires a non-negative device_id"); } + pipeline_contract_ = {PTO_PIPELINE_CONTRACT_ABI_VERSION, 0, 1, {}}; // libsimpler_log.so (RTLD_GLOBAL, with HostLogger already seeded via // simpler_log_init) and — on sim — libcpu_sim_context.so (RTLD_GLOBAL) must @@ -94,6 +105,7 @@ void ChipWorker::init( throw std::runtime_error(err); } + GetPipelineContractFn get_pipeline_contract_fn = nullptr; try { create_device_context_fn_ = load_symbol(handle, "create_device_context"); destroy_device_context_fn_ = load_symbol(handle, "destroy_device_context"); @@ -105,6 +117,7 @@ void ChipWorker::init( simpler_init_fn_ = load_symbol(handle, "simpler_init"); register_callable_fn_ = load_symbol(handle, "simpler_register_callable"); run_fn_ = load_symbol(handle, "simpler_run"); + get_pipeline_contract_fn = load_optional_symbol(handle, "get_pipeline_contract"); unregister_callable_fn_ = load_symbol(handle, "simpler_unregister_callable"); get_aicpu_dlopen_count_fn_ = load_symbol(handle, "get_aicpu_dlopen_count"); get_host_dlopen_count_fn_ = load_symbol(handle, "get_host_dlopen_count"); @@ -134,6 +147,16 @@ void ChipWorker::init( throw; } + PipelineContract resolved_contract{PTO_PIPELINE_CONTRACT_ABI_VERSION, 0, 1, {}}; + if (get_pipeline_contract_fn != nullptr) { + const PipelineContract *contract = get_pipeline_contract_fn(); + if (!is_valid_depth1_pipeline_contract(contract)) { + dlclose(handle); + throw std::runtime_error("host runtime returned a PipelineContract this build cannot accept"); + } + resolved_contract = *contract; + } + lib_handle_ = handle; device_ctx_ = create_device_context_fn_(); @@ -252,6 +275,10 @@ void ChipWorker::init( } device_id_ = device_id; + // Published only once the runtime is up: the rollback paths above leave the + // default K=1 contract in place, so a failed init never reports the counts + // of a runtime this worker is not bound to. + pipeline_contract_ = resolved_contract; initialized_ = true; // Provision async-DMA workspaces (SDMA) once, now that the device is up. The @@ -313,6 +340,7 @@ void ChipWorker::finalize() { comm_barrier_fn_ = nullptr; comm_destroy_fn_ = nullptr; runtime_buf_.clear(); + pipeline_contract_ = {PTO_PIPELINE_CONTRACT_ABI_VERSION, 0, 1, {}}; initialized_ = false; device_id_ = -1; finalized_ = true; diff --git a/src/common/worker/chip_worker.h b/src/common/worker/chip_worker.h index a09108143..472f9e1ed 100644 --- a/src/common/worker/chip_worker.h +++ b/src/common/worker/chip_worker.h @@ -140,6 +140,7 @@ class ChipWorker { int device_id() const { return device_id_; } bool initialized() const { return initialized_; } + unsigned pipeline_depth() const { return pipeline_contract_.pipeline_depth; } private: using CreateDeviceContextFn = void *(*)(); @@ -157,6 +158,7 @@ class ChipWorker { ); using SimplerRegisterCallableFn = int (*)(void *, int32_t, const void *); using SimplerRunFn = int (*)(void *, void *, int32_t, const void *, const CallConfig *); + using GetPipelineContractFn = const PipelineContract *(*)(); using SimplerUnregisterCallableFn = int (*)(void *, int32_t); using GetAicpuDlopenCountFn = size_t (*)(void *); using SimplerProvisionDmaWorkspaceFn = int (*)(void *, uint32_t); @@ -226,6 +228,7 @@ class ChipWorker { uint64_t base_comm_handle_ = 0; std::vector runtime_buf_; + PipelineContract pipeline_contract_{PTO_PIPELINE_CONTRACT_ABI_VERSION, 0, 1, {}}; // device_id_ is set once in init() and never modified afterward. All // ChipWorker callers run on the thread that called init() (the same // thread is the only one that subsequently calls malloc / copy_to / diff --git a/src/common/worker/pipeline_contract.h b/src/common/worker/pipeline_contract.h new file mode 100644 index 000000000..7b7dd9d03 --- /dev/null +++ b/src/common/worker/pipeline_contract.h @@ -0,0 +1,54 @@ +/* + * 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. + * ----------------------------------------------------------------------------------------------------------- + */ + +/** + * Admission check for the PipelineContract a host runtime declares. + * + * Lives beside the contract rather than inside ChipWorker so the rules a build + * will honor are stated in one place and can be exercised on their own. + */ + +#ifndef SRC_COMMON_WORKER_PIPELINE_CONTRACT_H_ +#define SRC_COMMON_WORKER_PIPELINE_CONTRACT_H_ + +#include + +#include "pto_runtime_c_api.h" + +/** + * Whether this build can honor `contract`. + * + * Rejects a contract whose ABI version is not the one compiled in, that + * declares more resources than fit, that asks for a pipeline depth other than + * 1, or whose resources carry an unspecified or out-of-range kind, an + * out-of-range class, or a non-zero reserved `bytes_per_copy`. + * + * The unspecified-kind rule is what catches a `resource_count` larger than the + * entries a runtime actually filled in: the trailing entries are still zeroed, + * and zero is not a resource. Repeated kinds stay legal, so a runtime may + * declare several resources of one kind. + */ +inline bool is_valid_depth1_pipeline_contract(const PipelineContract *contract) { + if (contract == nullptr || contract->abi_version != PTO_PIPELINE_CONTRACT_ABI_VERSION || + contract->resource_count > PTO_PIPELINE_MAX_RESOURCES || contract->pipeline_depth != 1) { + return false; + } + for (uint32_t i = 0; i < contract->resource_count; ++i) { + const PipelineResource &resource = contract->resources[i]; + if (resource.kind == PTO_PIPELINE_KIND_UNSPECIFIED || resource.kind > PTO_PIPELINE_AICORE_STREAM || + resource.resource_class > PTO_PIPELINE_EXEC_HANDLE || resource.bytes_per_copy != 0) { + return false; + } + } + return true; +} + +#endif // SRC_COMMON_WORKER_PIPELINE_CONTRACT_H_ diff --git a/src/common/worker/pto_runtime_c_api.h b/src/common/worker/pto_runtime_c_api.h index 38035da60..29c236412 100644 --- a/src/common/worker/pto_runtime_c_api.h +++ b/src/common/worker/pto_runtime_c_api.h @@ -15,10 +15,9 @@ * Both the ChipWorker (consumer, resolves public symbols via dlsym) and the * platform implementations (producers, define all symbols) include this file. * - * Public API — resolved by ChipWorker via dlsym (every host_runtime.so must - * export ALL of these; runtimes without a real backend ship not-supported - * stubs rather than omitting symbols, so ChipWorker can dlsym the full set - * unconditionally without per-symbol probing): + * Required API — resolved by ChipWorker via dlsym (every host_runtime.so must + * export all of these; runtimes without a real backend ship not-supported + * stubs rather than omitting symbols): * - lifecycle: create_device_context, destroy_device_context, * simpler_init, finalize_device * - sizing: get_runtime_size @@ -32,6 +31,9 @@ * - comm: comm_init, comm_alloc_windows, comm_get_local_window_base, * comm_get_window_size, comm_barrier, comm_destroy * + * Optional metadata: + * - pipeline: get_pipeline_contract + * * Memory management: caller allocates a buffer of get_runtime_size() bytes * and passes it to simpler_run(). Error codes: 0 = success, negative = error. */ @@ -61,6 +63,65 @@ enum { PTO_RUNTIME_ERR_UNSUPPORTED = -2, }; +enum { + PTO_PIPELINE_CONTRACT_ABI_VERSION = 1, + PTO_PIPELINE_MAX_RESOURCES = 8, + /* Ceiling on pipeline_depth once a depth above 1 is enabled. */ + PTO_PIPELINE_MAX_DEPTH = 2, +}; + +/** + * How a resource behaves across the KernelLaunch boundary, which is what + * decides its copy count: HOST_PER_RUN and EXEC_HANDLE need one instance per + * in-flight run (`pipeline_depth`), DEVICE_SCRATCH needs exactly one. + */ +typedef enum PipelineResourceClass { + /* Carries this run's own content, so the device is still reading the + previous run's content while the next run is prepared. */ + PTO_PIPELINE_HOST_PER_RUN = 0, + /* Not rewritten per run: whoever populates it does so once, and device ops + run one at a time, so a single instance is reused across runs. */ + PTO_PIPELINE_DEVICE_SCRATCH = 1, + /* Execution context (stream) a run owns while its op runs and is reaped. */ + PTO_PIPELINE_EXEC_HANDLE = 2, +} PipelineResourceClass; + +typedef enum PipelineResourceKind { + /* Zero is not a resource: it is what an entry a runtime never filled in + reads as, so a declaration that overstates resource_count is rejected + instead of decaying into a valid-looking resource. */ + PTO_PIPELINE_KIND_UNSPECIFIED = 0, + PTO_PIPELINE_GM_HEAP = 1, + PTO_PIPELINE_GM_SM = 2, + PTO_PIPELINE_RUNTIME_IMAGE = 3, + PTO_PIPELINE_TASK_ARGS = 4, + PTO_PIPELINE_AICPU_STREAM = 5, + PTO_PIPELINE_AICORE_STREAM = 6, +} PipelineResourceKind; + +typedef struct PipelineResource { + uint32_t kind; + uint32_t resource_class; + /* Size of one copy. Reserved: currently declared as 0 and required to be 0. */ + uint64_t bytes_per_copy; +} PipelineResource; + +/** + * Runtime-owned declaration of resources that cross KernelLaunch. + * + * `pipeline_depth` is the only replication count: a resource needs + * `pipeline_depth` copies unless its class is DEVICE_SCRATCH, which needs one. + * Per-resource copy counts stay derivable from the class, so a runtime that + * wants a cheap resource replicated and an expensive one shared says so by + * classifying them, not by carrying a second global count. + */ +typedef struct PipelineContract { + uint32_t abi_version; + uint32_t resource_count; + uint32_t pipeline_depth; + PipelineResource resources[PTO_PIPELINE_MAX_RESOURCES]; +} PipelineContract; + /* Per-stage run timing is no longer returned. The platform emits it as * `[STRACE]` log markers (host stages + the AICPU device-phase breakdown, * gated on SIMPLER_HOST_STRACE) — parse with simpler_setup.tools.strace_timing. @@ -70,6 +131,9 @@ enum { * Public API (resolved by ChipWorker via dlsym) * =========================================================================== */ +/** Return this runtime's immutable pipeline resource declaration. Optional. */ +const PipelineContract *get_pipeline_contract(void); + /** * Create a new device context (heap-allocated DeviceRunner). * Each ChipWorker should own one context for the lifetime of its init→finalize cycle. diff --git a/tests/ut/cpp/CMakeLists.txt b/tests/ut/cpp/CMakeLists.txt index 7b164beed..d4da0598a 100644 --- a/tests/ut/cpp/CMakeLists.txt +++ b/tests/ut/cpp/CMakeLists.txt @@ -339,6 +339,7 @@ add_hierarchical_test(test_orchestrator hierarchical/test_orchestrator.cpp) add_hierarchical_test(test_scheduler hierarchical/test_scheduler.cpp) add_hierarchical_test(test_remote_wire hierarchical/test_remote_wire.cpp) add_hierarchical_test(test_remote_endpoint hierarchical/test_remote_endpoint.cpp) +add_hierarchical_test(test_pipeline_contract hierarchical/test_pipeline_contract.cpp) # --------------------------------------------------------------------------- # Types / task_interface tests (src/common/task_interface/) diff --git a/tests/ut/cpp/hierarchical/test_pipeline_contract.cpp b/tests/ut/cpp/hierarchical/test_pipeline_contract.cpp new file mode 100644 index 000000000..bd795a2a2 --- /dev/null +++ b/tests/ut/cpp/hierarchical/test_pipeline_contract.cpp @@ -0,0 +1,134 @@ +/* + * 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 "pipeline_contract.h" + +namespace { + +// A contract a runtime could legitimately ship today: one host-filled region, +// one device-built region, and the two execution handles. +PipelineContract accepted_contract() { + PipelineContract c{}; + c.abi_version = PTO_PIPELINE_CONTRACT_ABI_VERSION; + c.resource_count = 4; + c.pipeline_depth = 1; + c.resources[0] = {PTO_PIPELINE_TASK_ARGS, PTO_PIPELINE_HOST_PER_RUN, 0}; + c.resources[1] = {PTO_PIPELINE_RUNTIME_IMAGE, PTO_PIPELINE_DEVICE_SCRATCH, 0}; + c.resources[2] = {PTO_PIPELINE_AICPU_STREAM, PTO_PIPELINE_EXEC_HANDLE, 0}; + c.resources[3] = {PTO_PIPELINE_AICORE_STREAM, PTO_PIPELINE_EXEC_HANDLE, 0}; + return c; +} + +TEST(PipelineContract, AcceptsADeclarationThisBuildCanHonor) { + const PipelineContract c = accepted_contract(); + EXPECT_TRUE(is_valid_depth1_pipeline_contract(&c)); +} + +// A runtime that exports no contract is handled by the caller, not here. +TEST(PipelineContract, RejectsNull) { EXPECT_FALSE(is_valid_depth1_pipeline_contract(nullptr)); } + +TEST(PipelineContract, AcceptsAnEmptyResourceList) { + PipelineContract c = accepted_contract(); + c.resource_count = 0; + EXPECT_TRUE(is_valid_depth1_pipeline_contract(&c)); +} + +TEST(PipelineContract, RejectsAnotherAbiVersion) { + PipelineContract c = accepted_contract(); + c.abi_version = PTO_PIPELINE_CONTRACT_ABI_VERSION + 1; + EXPECT_FALSE(is_valid_depth1_pipeline_contract(&c)); + c.abi_version = 0; + EXPECT_FALSE(is_valid_depth1_pipeline_contract(&c)); +} + +TEST(PipelineContract, RejectsMoreResourcesThanFit) { + PipelineContract c = accepted_contract(); + c.resource_count = PTO_PIPELINE_MAX_RESOURCES + 1; + EXPECT_FALSE(is_valid_depth1_pipeline_contract(&c)); +} + +// The depth-1 gate is what keeps this build from honoring a contract whose +// resources it would have to replicate. +TEST(PipelineContract, RejectsADepthOtherThanOne) { + PipelineContract c = accepted_contract(); + c.pipeline_depth = 0; + EXPECT_FALSE(is_valid_depth1_pipeline_contract(&c)); + c.pipeline_depth = 2; + EXPECT_FALSE(is_valid_depth1_pipeline_contract(&c)); + c.pipeline_depth = PTO_PIPELINE_MAX_DEPTH; + EXPECT_FALSE(is_valid_depth1_pipeline_contract(&c)); +} + +TEST(PipelineContract, RejectsAnOutOfRangeKindOrClass) { + PipelineContract c = accepted_contract(); + c.resources[0].kind = PTO_PIPELINE_AICORE_STREAM + 1; + EXPECT_FALSE(is_valid_depth1_pipeline_contract(&c)); + + c = accepted_contract(); + c.resources[0].resource_class = PTO_PIPELINE_EXEC_HANDLE + 1; + EXPECT_FALSE(is_valid_depth1_pipeline_contract(&c)); +} + +// bytes_per_copy is reserved: nothing sizes anything from it yet, so a runtime +// that populates it is declaring a contract this build does not implement. +TEST(PipelineContract, RejectsANonZeroReservedSize) { + PipelineContract c = accepted_contract(); + c.resources[1].bytes_per_copy = 4096; + EXPECT_FALSE(is_valid_depth1_pipeline_contract(&c)); +} + +TEST(PipelineContract, RejectsAnUnspecifiedKind) { + PipelineContract c = accepted_contract(); + c.resources[1].kind = PTO_PIPELINE_KIND_UNSPECIFIED; + EXPECT_FALSE(is_valid_depth1_pipeline_contract(&c)); +} + +// A resource_count larger than the entries a runtime filled in leaves trailing +// zeroed entries, and zero is not a resource. This must hold whatever the +// filled entries are — a rule keyed on a collision with the first kind would +// pass only when the declaration happens to use that kind. +TEST(PipelineContract, RejectsAResourceCountPastTheFilledEntries) { + for (uint32_t filled : + {static_cast(PTO_PIPELINE_GM_HEAP), static_cast(PTO_PIPELINE_TASK_ARGS)}) { + PipelineContract c{}; + c.abi_version = PTO_PIPELINE_CONTRACT_ABI_VERSION; + c.pipeline_depth = 1; + c.resources[0] = {filled, PTO_PIPELINE_HOST_PER_RUN, 0}; + + c.resource_count = 2; // overstates by exactly one + EXPECT_FALSE(is_valid_depth1_pipeline_contract(&c)) << "filled kind " << filled; + c.resource_count = 3; + EXPECT_FALSE(is_valid_depth1_pipeline_contract(&c)) << "filled kind " << filled; + } +} + +// A kind names a resource type, not one instance of it, so a runtime that needs +// two of a kind — two AICore streams for parallel branches, say — can say so. +TEST(PipelineContract, AcceptsARepeatedKind) { + PipelineContract c = accepted_contract(); + c.resources[3].kind = PTO_PIPELINE_AICPU_STREAM; + EXPECT_TRUE(is_valid_depth1_pipeline_contract(&c)); +} + +TEST(PipelineContract, AcceptsEveryKindOnce) { + PipelineContract c{}; + c.abi_version = PTO_PIPELINE_CONTRACT_ABI_VERSION; + c.pipeline_depth = 1; + c.resource_count = PTO_PIPELINE_AICORE_STREAM; + for (uint32_t kind = PTO_PIPELINE_GM_HEAP; kind <= PTO_PIPELINE_AICORE_STREAM; ++kind) { + c.resources[kind - 1] = {kind, PTO_PIPELINE_HOST_PER_RUN, 0}; + } + EXPECT_TRUE(is_valid_depth1_pipeline_contract(&c)); +} + +} // namespace