Skip to content

Add: kernel callable registration and generation guards - #2190

Draft
YunjiQin wants to merge 3 commits into
hw-native-sys:mainfrom
YunjiQin:feat/k10a-callable-cache
Draft

Add: kernel callable registration and generation guards#2190
YunjiQin wants to merge 3 commits into
hw-native-sys:mainfrom
YunjiQin:feat/k10a-callable-cache

Conversation

@YunjiQin

@YunjiQin YunjiQin commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Each successful kernel prepare_callable call uploads and registers a new callable and returns a fresh context-local SimplerCallableHandle {callable_id, generation}, even when both the content and input pointer match an earlier call. The caller owns handle reuse and content deduplication; simpler does not search existing content or return a previous handle.

Registrations occupy independent device addresses and consume independent slots and bytes in a 64-slot, 512 MiB resident arena. The 65th registration is rejected even for identical content. Capacity failures preserve existing handles, and launch rejects missing or stale handles. There is no eviction or slot reuse. The runtime registration bridge consumes only the current pending upload, never an old hash-matched image.

A separate simpler_aicpu_kernel_exec entry validates the current device residency before entering the payload consumer. Production-entry UT covers stale snapshots after a slot generation change.

Dependencies and scope

  • Depends on Add: persistent kernel-context execution resources (K2) #2176 (K2), which depends on Add: kernel-mode C ABI skeleton and wire headers (K1) #2064 (K1). This draft retains the tested K2 base 48b120b1; dependency commits are separate from the K10a commit.
  • K10a-only diff: review the independent commit.
  • K2's asynchronous preparation changes are outside this PR; existing preparation synchronization is unchanged.
  • Program-mode registration retains its existing behavior.
  • Host binder and runtime-specific payload execution remain unconnected. The production payload consumer returns UnsupportedPayload, and kernel support remains disabled. Device-entry validation is covered by CPU UT; real-device ACLGraph replay and its CANN error propagation are not yet validated.
  • Chinese call-chain documentation: docs/zh-cn/kernel-callable-residency.md.

Validation

  • All pre-commit checks pass.
  • Seven relevant C++ test targets pass. Repeated identical content produces distinct IDs and device addresses, counts toward both limits, and cannot be admitted when full.
  • Callable-cache tests pass ASan/UBSan; dispatch tests also passed ASan/UBSan.
  • Python/C ABI/callable-identity regression: 114 passed, 36 skipped.
  • A3 onboard kernel C ABI tests: 28 passed, 16 deselected. Both runtimes register identical content 64 times, return IDs 0–63, and report 64 dlopens before rejecting another registration.
  • All eight runtime build combinations succeed. The AICPU entry is exported for both architectures, backends, and runtimes.

sunkaixuan2018 and others added 2 commits September 9, 2026 15:44
Kernel mode is simpler's second execution identity: instead of owning
the device, a context borrows the caller's already-current device and
stream to enqueue one bounded asynchronous operator per launch, so a
PyPTO program is capturable by ACLGraph as an ordinary node. This
change freezes the public surface that identity hangs off and gives the
context a write-once identity the guards can key on. It creates no
resources. The program path gains one call — simpler_init latches
PROGRAM — and no behavior: latching a fresh context always succeeds, is
idempotent, and nothing on that path reads the latch.

- runtime_c_api.h declares the lifecycle entries
  simpler_kernel_mode_{supported,init,prepare_callable,launch} and adds
  the host-band code PTO_RUNTIME_ERR_INVALID_STATE. The existing
  finalize_device stays the fifth lifecycle entry, and a kernel context
  now reaches it. Kernel-mode capacity is
  a mode invariant rather than a gated state: config is context-static,
  so each pooled arena region is committed at most once, and
  setup_static_arena reports a grow or release request on a committed
  region under kernel mode as an internal invariant break; capacity
  intent travels in CallConfig.runtime_env like everywhere else.
- Execution identity is a write-once property of the context rather
  than a state that evolves. ExecutionModeLatch (platform/include/host/
  execution_mode_latch.h) replaces the four-state claim: the first init
  entry to run latches the mode, and it never changes — not on finalize,
  not on error. simpler_init latches PROGRAM before touching any
  process or runner state, so the program/kernel mutual exclusion is
  enforced on every program init instead of resting on a separate
  declaration call. There is no unlatch, which the latch documents as a
  consequence: a handle from a failed kernel init can never be recycled
  into a program context. SimplerExecutionMode now has one definition
  (task_interface/execution_mode.h) that both the wire header and the
  latch consume, so the host-side identity and the value that travels to
  the AICPU can no longer disagree.
- device_id_ records which device a context is on, not a claim on it —
  ownership is what the latch carries. attach_current_thread splits
  accordingly: bind_current_thread does the per-thread rtSetDevice and
  nothing else; attach_current_thread is the program-mode adopt
  (bind plus the one-shot op-execute watchdog and identity write) and
  refuses on a kernel latch; adopt_borrowed_device records the device a
  kernel context runs on without binding the thread and without
  configure_aicore_op_timeout, whose aclrtSetOpExecuteTimeOutV2 would
  rewrite the watchdog for every other user of a borrowed card. It does
  resolve the timeout config, because the stream and scheduler timeouts
  derived from it are read on both identities. DeviceRunner::finalize()
  is the one caller that runs under both identities and skips the bind
  on a kernel latch, so the kernel close path reaches its no-reset
  branch instead of being turned away by a device bind it never needed.
- ensure_acl_ready(), force_reset_device(), and finalize()'s rt-layer
  device reset refuse on a kernel-mode context (a2a3 + a5): the ACL
  lifecycle belongs to the caller, and every call site of the five ACL
  lifecycle APIs falls into three enumerable classes (below the
  ensure_acl_ready guard, inside force_reset_device behind its own
  guard, or gated on acl_ready_ which only the guarded path sets), with
  finalize's rt-layer reset intercepted by its own kernel-mode branch —
  so poison recovery can never reset the device out from under the
  host process.
- kernel_invocation_header.h pins the envelope every kernel launch
  ships to the AICPU (mode / callable / generation / payload length /
  int32_t arg counts). Both sides of the wire come from one
  build_runtimes.py build, so the struct carries no version or size
  negotiation and the POD/standard-layout guards are its only
  compile-time checks. generation is the occupancy counter of the
  residency slot callable_id resolves to - a property of the slot, not
  of the callable in it, so a generation carried by the callable could
  not detect slot reuse - with zero reserved for "not recorded".
  ChipCallable's sig_count includes the scalar entries and its
  scalar_count reads 0 both for a scalar-free orchestration and for an
  artifact built before the field existed, so a consumer derives the
  effective scalar count - the field when nonzero, otherwise the
  signature's SCALAR entries, the split count_callable_tensor_args
  already computes - and compares tensor_count against sig_count minus
  it. Subtracting the field directly would count an unrecorded
  callable's scalars as tensors.
- Kernel-entry argument validation is shared by all eight host-runtime
  components through kernel_entry_validation.h (one copy of the
  null/range/image-size/alignment checks; a binary pointer and its size
  must be present or absent together, and a callable image must be
  aligned for ChipCallable so its CALLABLE_CHILD_ALIGN-relative storage_
  lands aligned too), so a stub and a real implementation accept and
  reject exactly the same arguments.
- KernelExecutionState and ExecutionModeClaimState carry the kernel
  context phase machine (New/Collecting/ReadyEnqueued/Poisoned/
  Closing/Closed with sticky, retriable Closing and separate
  runtime-error and teardown-error slots) and the two restricted
  operation vocabularies; synchronize, allocation, capture queries,
  and model attachment stay unrepresentable in those tables, and a
  launch implementation is obligated to route through them. Every
  kernel-mode guard reads the identity through ExecutionModeLatch::
  is_kernel() rather than comparing an enumerator at the call site, so
  the test lives in one place instead of eight.
- ChipWorker dlsyms the four new symbols from every runtime, so a
  component missing one fails at load, and clears them alongside the
  other resolved pointers on all three teardown paths so none is left
  dangling into the library DlHandleGuard dlcloses.
  test_host_runtime_abi.py
  asserts the export across all eight components, and table-driven UTs
  cover the phase machine (including failed-rollback landing in
  Closing with the create error reported and the cleanup error
  latched), the shared argument validation, and the wire layout.
- ChipCallable additionally records scalar_count as a cached
  derivation of the signature's SCALAR entries: make_callable rejects
  a nonzero count that disagrees with the signature, while 0 also
  means "not recorded" (legacy blobs read 0). The field occupies four
  bytes of historical header tail padding, so every historical offset,
  sizeof, and the kernel-cache ABI token are unchanged;
  ChipCallable.build gains a trailing scalar_count=0 keyword and a
  read-only property.

Two facts a reader should not have to re-derive. The latch refusal returns
PTO_RUNTIME_ERR_INVALID_STATE (-1003) rather than PTO_RUNTIME_ERR_INTERNAL
(-1000) on purpose: conftest.py scrapes "simpler_init failed with code <N>"
and treats -1000 as a poisoned card, so an identity conflict must not look
like one. And kernel_execution_state.cpp stays compiled into all four host
runtimes even though grepping KernelExecutionState now finds only its own
header and .cpp — it is the persistent-state change's foundation, not an
orphaned translation unit.

Every kernel-mode branch this adds is provably dead in this commit: no
production site latches KERNEL (`git grep 'latch(SIMPLER_MODE_KERNEL)' src`
is empty) because both simpler_kernel_mode_init stubs return before any latch
call, so is_kernel() is false on every context and the program path takes the
same branch it took before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Allocate the device Runtime image, register table and KernelArgs once per
context, and own the dedicated AICPU stream, hidden AICore stream and
context events until explicit close.

Mount init and callable preparation on the write-once execution-mode
latch. Preserve device identity and runtime ownership on failed teardown,
reject repeated initialization before executor mutation, and leave the
caller device binding and ACL lifecycle untouched.

Cover lifecycle call order, creation rollback, callable preparation,
unclosed destruction, retryable close, and all six ACL/reset interceptors.
Exercise the real fatal-device finalizer using an injected drain result
without poisoning or resetting a physical device. Launch remains a stub.
@coderabbitai

coderabbitai Bot commented Sep 10, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@nalinaly nalinaly left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

请调整 callable 驻留容量及 device 内存分配方式:8192 项规格,参考 CANN 的 2 MiB 分块按需增长。具体意见见行内评论。

class KernelCallableCache {
public:
static constexpr size_t kDescriptorBytes = MAX_REGISTERED_CALLABLE_IDS * sizeof(KernelCallableDeviceResidency);
static constexpr size_t kByteLimit = 512ULL * 1024 * 1024;

@nalinaly nalinaly Sep 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

建议将 kernel 模式的驻留 callable 容量规格调整为 8192,并将条目规格与 device 内存预分配解耦。

当前实现存在两个问题:第 65 个不同 callable 会在仍有可用 device 内存时被拒绝;第一个很小的 callable 也会触发约 512 MiB 的 arena 分配。仅把 64 调大、继续保留这个 arena 模型,不能满足大量 kernel 按需注册的场景。

建议本 PR 按以下规格修改:

  1. 最多支持 8192 个不同的驻留 callable。 需要同步处理 Host cache/resolve、runner、A2A3/A5 AICPU 注册表和 Device dispatch 的相关边界,不能只修改 cache 一处;8192 是条目容量规格,不代表预分配 8192 套 device 执行资源。
  2. Device 内存按实际驻留需求申请。 可参考 CANN runtime 的 2 MiB 分块池:小镜像在已有块内子分配,空间不足才追加新块;超过单块大小的镜像按实际大小加必要对齐独立分配。2 MiB 是共享分配块的粒度,不是每个 callable 至少占 2 MiB。取消固定 512 MiB 首次预留,按需增长。
  3. 驻留代码缓存的 device 总容量上限暂定为 2 GiB。 这是容量上限,不是首次预分配量;仍按上述方式按需申请。达到 8192 项或 2 GiB 容量上限时返回明确的容量错误,不影响已经成功驻留的条目。
  4. 扩容只追加,不迁移已发布的代码和 descriptor。 保持已有地址及 handle 有效;新增分配、上传和注册仍放在 capture 外的 prepare,launch/replay 不承担扩容工作。

Host 内存目前不是本条 review 的关注点,现有 Host 镜像保留方式可以不改。

参考 CANN 官方源码:BinaryLoad 按实际加载大小分配2 MiB 单池定义池不足时追加新池

@YunjiQin
YunjiQin force-pushed the feat/k10a-callable-cache branch from 2c87647 to 05a2471 Compare September 14, 2026 03:36
@YunjiQin YunjiQin changed the title Add: kernel callable residency cache and generation guards Add: kernel callable registration and generation guards Sep 14, 2026
Register each kernel callable preparation independently and return a
fresh context-local ID/generation handle, even for identical content.
Keep handle reuse and content deduplication in the caller.

Bound resident registrations to 64 slots and a 512 MiB arena. Upload
and retain each image separately, preserve existing handles on admission
failure, and reject missing or stale handles before dispatch.

Add an AICPU invocation entry that checks current device residency
before payload consumption. Keep unsupported execution explicit until
the binder and runtime payload consumers are connected.

Test repeated identical registrations, capacity boundaries, rollback,
stale handles and device-entry validation. Document the call chain.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants