Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/user/reference/python-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,15 @@ ChipCallable.build(
func_name="my_orchestration", # the exported orchestration symbol
binary=orch_bytes,
children=[(func_id, core_callable), ...],
scalar_count=0, # scalar arguments the orchestration expects
)
```

`ArgDirection` is `SCALAR`, `IN`, `OUT`, or `INOUT`. The signature list is
positional and defines the task-arg order. `func_id` must match the id the
orchestration submits. `ChipCallable` exposes `binary_size`.
orchestration submits. `ChipCallable` exposes `binary_size` and
`scalar_count`; a `scalar_count` of 0 means either an artifact built before
the count was recorded or an orchestration that takes no scalars.

For L3+ graph construction, `TaskArgs.add_dep(*handles)` adds `WAIT | RETAIN`
edges: each consumer waits for its producers and keeps their task-owned
Expand Down
21 changes: 16 additions & 5 deletions python/bindings/task_interface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2465,7 +2465,8 @@ NB_MODULE(_task_interface, m) {
.def_static(
"build",
[](std::vector<ArgDirection> signature, std::string func_name, nb::bytes binary,
std::vector<std::tuple<int32_t, PyCoreCallable>> children, std::string config_name) -> PyChipCallable {
std::vector<std::tuple<int32_t, PyCoreCallable>> children, std::string config_name,
int32_t scalar_count) -> PyChipCallable {
auto bin_ptr = reinterpret_cast<const void *>(binary.c_str());
auto bin_size = static_cast<uint32_t>(binary.size());
auto child_count = static_cast<int32_t>(children.size());
Expand All @@ -2478,14 +2479,15 @@ NB_MODULE(_task_interface, m) {
}

auto buf = make_callable<CoreCallable, CHIP_MAX_TENSOR_ARGS, 1024>(
signature.data(), static_cast<int32_t>(signature.size()), func_name.c_str(), bin_ptr, bin_size,
func_ids.data(), child_bufs.data(), child_count, config_name.c_str()
signature.data(), static_cast<int32_t>(signature.size()), scalar_count, func_name.c_str(), bin_ptr,
bin_size, func_ids.data(), child_bufs.data(), child_count, config_name.c_str()
);
return PyChipCallable{std::move(buf)};
},
nb::arg("signature"), nb::arg("func_name"), nb::arg("binary"), nb::arg("children"),
nb::arg("config_name") = "",
"Build a ChipCallable from signature, func_name, binary, and list of (func_id, CoreCallable) children."
nb::arg("config_name") = "", nb::arg("scalar_count") = 0,
"Build a ChipCallable from signature, func_name, binary, and list of (func_id, CoreCallable) children. "
"scalar_count records how many scalar arguments the orchestration expects (0 when it takes none)."
)

.def_static(
Expand Down Expand Up @@ -2553,6 +2555,15 @@ NB_MODULE(_task_interface, m) {
"The optional orchestration config function name."
)

.def_prop_ro(
"scalar_count",
[](const PyChipCallable &self) -> int32_t {
return self.get().scalar_count();
},
"Number of scalar arguments the orchestration expects. 0 also for "
"legacy artifacts built before the count was recorded."
)

.def_prop_ro(
"child_count",
[](const PyChipCallable &self) -> int32_t {
Expand Down
62 changes: 57 additions & 5 deletions src/common/task_interface/callable.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
#include <cstring>
#include <stdexcept>
#include <string>
#include <type_traits>
#include <vector>

#include "arg_direction.h"
Expand Down Expand Up @@ -108,6 +109,10 @@ struct Callable {
int32_t child_count_;
char config_name_[CALLABLE_FUNC_NAME_MAX];
uint32_t config_name_len_;
// Occupies four bytes of the historical padding between config_name_len_
// and the 16-byte-aligned storage_, so every other field keeps its wire
// offset and a legacy zero-initialized blob reads as scalar_count == 0.
int32_t scalar_count_;
// Children live in storage_ at CALLABLE_ALIGN-aligned offsets, but the
// all-uint32 header above can leave offsetof(storage_) at 4-mod-8, which
// would place an 8-byte-aligned Child (CoreCallable has a uint64) on a
Expand All @@ -129,6 +134,9 @@ struct Callable {
uint32_t func_name_len() const { return func_name_len_; }
const char *config_name() const { return config_name_; }
uint32_t config_name_len() const { return config_name_len_; }
// 0 means either an artifact built before this field existed or an
// orchestration that takes no scalars; the two are indistinguishable.
int32_t scalar_count() const { return scalar_count_; }

const Child &child(int32_t i) const {
if (i < 0 || i >= child_count_) throw std::out_of_range("Callable: child index out of range");
Expand All @@ -149,9 +157,9 @@ struct Callable {

template <typename C, int MS, int MC>
friend std::vector<uint8_t> make_callable(
const ArgDirection *sig, int32_t sig_count, const char *func_name, const void *binary, uint32_t binary_size,
const int32_t *child_func_ids, const std::vector<uint8_t> *child_buffers, int32_t child_count,
const char *config_name
const ArgDirection *sig, int32_t sig_count, int32_t scalar_count, const char *func_name, const void *binary,
uint32_t binary_size, const int32_t *child_func_ids, const std::vector<uint8_t> *child_buffers,
int32_t child_count, const char *config_name
);
};

Expand All @@ -177,6 +185,43 @@ static_assert(
"ChipCallable.storage_ must be CALLABLE_CHILD_ALIGN-aligned for SIMT kernel binaries"
);

// Callable bytes are shipped through L3/L4 IPC and the on-disk kernel cache,
// so the header layout is wire ABI. scalar_count_ must consume tail padding
// rather than move any historical field. The constants below encode
// CHIP_MAX_TENSOR_ARGS = 256, CORE_MAX_TENSOR_ARGS = 32,
// CALLABLE_FUNC_NAME_MAX = 64, and MaxChildren = 1024; a deliberate capacity
// change updates them in the same commit.
static_assert(
std::is_trivially_copyable_v<ChipCallable> && std::is_standard_layout_v<ChipCallable>,
"ChipCallable wire ABI: must stay memcpy-able POD"
);
static_assert(
std::is_trivially_copyable_v<CoreCallable> && std::is_standard_layout_v<CoreCallable>,
"CoreCallable wire ABI: must stay memcpy-able POD"
);
static_assert(offsetof(ChipCallable, signature_) == 0, "ChipCallable wire ABI: signature offset changed");
static_assert(offsetof(ChipCallable, sig_count_) == 1024, "ChipCallable wire ABI: sig_count offset changed");
static_assert(offsetof(ChipCallable, binary_size_) == 1028, "ChipCallable wire ABI: binary_size offset changed");
static_assert(offsetof(ChipCallable, func_name_) == 1032, "ChipCallable wire ABI: func_name offset changed");
static_assert(offsetof(ChipCallable, func_name_len_) == 1096, "ChipCallable wire ABI: func_name_len offset changed");
static_assert(offsetof(ChipCallable, child_func_ids_) == 1100, "ChipCallable wire ABI: child_func_ids offset changed");
static_assert(offsetof(ChipCallable, child_offsets_) == 5196, "ChipCallable wire ABI: child_offsets offset changed");
static_assert(offsetof(ChipCallable, child_count_) == 9292, "ChipCallable wire ABI: child_count offset changed");
static_assert(offsetof(ChipCallable, config_name_) == 9296, "ChipCallable wire ABI: config_name offset changed");
static_assert(
offsetof(ChipCallable, config_name_len_) == 9360, "ChipCallable wire ABI: config_name_len offset changed"
);
static_assert(offsetof(ChipCallable, scalar_count_) == 9364, "ChipCallable wire ABI: scalar_count offset changed");
static_assert(offsetof(ChipCallable, storage_) == 9376, "ChipCallable wire ABI: storage offset changed");
static_assert(sizeof(ChipCallable) == 9376, "ChipCallable wire ABI: header size changed");
static_assert(offsetof(CoreCallable, signature_) == 0, "CoreCallable wire ABI: signature offset changed");
static_assert(offsetof(CoreCallable, sig_count_) == 128, "CoreCallable wire ABI: sig_count offset changed");
static_assert(offsetof(CoreCallable, binary_size_) == 132, "CoreCallable wire ABI: binary_size offset changed");
static_assert(offsetof(CoreCallable, resolved_addr_) == 136, "CoreCallable wire ABI: resolved_addr offset changed");
static_assert(offsetof(CoreCallable, storage_) == 144, "CoreCallable wire ABI: storage offset changed");
static_assert(sizeof(CoreCallable) == 144, "CoreCallable wire ABI: header size changed");
static_assert(CoreCallable::binary_data_offset() == 192, "CoreCallable wire ABI: binary data offset changed");

// ============================================================================
// Factory: make_callable for static leaf
// ============================================================================
Expand Down Expand Up @@ -216,8 +261,8 @@ make_callable(const ArgDirection *sig, int32_t sig_count, const void *binary, ui

template <typename Child, int MaxSig, int MaxChildren>
std::vector<uint8_t> make_callable(
const ArgDirection *sig, int32_t sig_count, const char *func_name, const void *binary, uint32_t binary_size,
const int32_t *child_func_ids, const std::vector<uint8_t> *child_buffers, int32_t child_count,
const ArgDirection *sig, int32_t sig_count, int32_t scalar_count, const char *func_name, const void *binary,
uint32_t binary_size, const int32_t *child_func_ids, const std::vector<uint8_t> *child_buffers, int32_t child_count,
// No default arg here: the friend declaration above has none, so a default
// on this definition is a "redeclaration may not have default arguments"
// error once ChipCallable is instantiated (the static_assert below does
Expand All @@ -231,6 +276,12 @@ std::vector<uint8_t> make_callable(
std::to_string(MaxSig)
);
}
if (scalar_count < 0 || scalar_count > CHIP_MAX_SCALAR_ARGS) {
throw std::invalid_argument(
"make_callable: requested scalar count " + std::to_string(scalar_count) +
" is outside the supported range [0, " + std::to_string(CHIP_MAX_SCALAR_ARGS) + "]"
);
}
if (child_count > MaxChildren) throw std::invalid_argument("make_callable: child_count exceeds MaxChildren");

using T = Callable<Child, MaxSig, MaxChildren>;
Expand All @@ -250,6 +301,7 @@ std::vector<uint8_t> make_callable(
for (int32_t i = 0; i < sig_count; ++i)
obj->signature_[i] = sig[i];
obj->sig_count_ = sig_count;
obj->scalar_count_ = scalar_count;
obj->binary_size_ = binary_size;

// Store func_name (null-terminated, truncated to CALLABLE_FUNC_NAME_MAX-1)
Expand Down
1 change: 1 addition & 0 deletions tests/ut/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,7 @@ set_tests_properties(test_sim_run_completion PROPERTIES LABELS "no_hardware")
add_task_interface_test(test_buffer types/test_buffer.cpp)
add_task_interface_test(test_child_memory types/test_child_memory.cpp)
add_task_interface_test(test_chip_max_tensor_args types/test_chip_max_tensor_args.cpp)
add_task_interface_test(test_callable_scalar_count types/test_callable_scalar_count.cpp)
add_task_interface_test(test_call_config types/test_call_config.cpp)

# Contract test for upload_chip_callable_buffer caller-buffer immutability.
Expand Down
100 changes: 100 additions & 0 deletions tests/ut/cpp/types/test_callable_scalar_count.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* 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.
* -----------------------------------------------------------------------------------------------------------
*/

// ChipCallable::scalar_count_ contract: the factory validates and records the
// count, the field occupies former header padding only, and a blob written by
// a producer that predates the field (whose padding make_callable
// zero-initialized) reads back as scalar_count == 0.

#include <cstddef>
#include <cstring>
#include <stdexcept>
#include <string>
#include <vector>

#include <gtest/gtest.h>

#include "callable.h"

namespace {

std::vector<uint8_t> build_chip_callable(int32_t scalar_count) {
ArgDirection sig[2] = {ArgDirection::IN, ArgDirection::OUT};

ArgDirection core_sig[1] = {ArgDirection::IN};
const uint8_t kernel[] = {0x01, 0x02, 0x03, 0x04};
auto core = make_callable<CORE_MAX_TENSOR_ARGS>(core_sig, 1, kernel, sizeof(kernel));

const uint8_t fake_orch_so[] = {0x7f, 'E', 'L', 'F'};
int32_t child_ids[1] = {3};
std::vector<uint8_t> children[1] = {std::move(core)};

return make_callable<CoreCallable, CHIP_MAX_TENSOR_ARGS, 1024>(
sig, 2, scalar_count, "orch_fn", fake_orch_so, sizeof(fake_orch_so), child_ids, children, 1, "cfg_name"
);
}

} // namespace

TEST(CallableScalarCount, RoundTripsThroughFactory) {
for (int32_t count : {0, 1, 7, CHIP_MAX_SCALAR_ARGS}) {
auto buf = build_chip_callable(count);
const auto *callable = reinterpret_cast<const ChipCallable *>(buf.data());
EXPECT_EQ(callable->scalar_count(), count);
}
}

TEST(CallableScalarCount, RejectsOutOfRangeWithValueAndLimit) {
for (int32_t count : {-1, CHIP_MAX_SCALAR_ARGS + 1}) {
try {
(void)build_chip_callable(count);
FAIL() << "expected scalar count validation to fail for " << count;
} catch (const std::invalid_argument &error) {
const std::string message = error.what();
EXPECT_NE(message.find(std::to_string(count)), std::string::npos) << message;
EXPECT_NE(message.find(std::to_string(CHIP_MAX_SCALAR_ARGS)), std::string::npos) << message;
}
}
}

// A legacy blob is byte-identical to a current one built with the same inputs
// except that the four bytes now holding scalar_count_ were zero-initialized
// padding. Zeroing them reproduces such a blob exactly.
TEST(CallableScalarCount, LegacyBlobReadsZero) {
auto buf = build_chip_callable(9);
std::memset(buf.data() + offsetof(ChipCallable, scalar_count_), 0, sizeof(int32_t));

const auto *callable = reinterpret_cast<const ChipCallable *>(buf.data());
EXPECT_EQ(callable->scalar_count(), 0);
EXPECT_EQ(callable->sig_count(), 2);
EXPECT_EQ(std::string(callable->func_name(), callable->func_name_len()), "orch_fn");
EXPECT_EQ(std::string(callable->config_name(), callable->config_name_len()), "cfg_name");
ASSERT_EQ(callable->child_count(), 1);
EXPECT_EQ(callable->child_func_id(0), 3);
EXPECT_EQ(callable->child(0).binary_size(), 4u);
}

// Two builds differing only in scalar_count must differ only in that field's
// four bytes — every other header byte, the orchestration binary, and the
// child payload keep their positions and values.
TEST(CallableScalarCount, OnlyTheFieldBytesVary) {
auto zero = build_chip_callable(0);
auto nine = build_chip_callable(9);
ASSERT_EQ(zero.size(), nine.size());

constexpr size_t field_begin = offsetof(ChipCallable, scalar_count_);
constexpr size_t field_end = field_begin + sizeof(int32_t);
for (size_t i = 0; i < zero.size(); ++i) {
if (i >= field_begin && i < field_end) continue;
ASSERT_EQ(zero[i], nine[i]) << "byte " << i << " must not depend on scalar_count";
}
EXPECT_NE(std::memcmp(zero.data() + field_begin, nine.data() + field_begin, sizeof(int32_t)), 0);
}
2 changes: 1 addition & 1 deletion tests/ut/cpp/types/test_chip_callable_upload_immutable.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ std::vector<uint8_t> build_test_chip_callable() {
std::vector<uint8_t> children[2] = {std::move(core0), std::move(core1)};

return make_callable<CoreCallable, CHIP_MAX_TENSOR_ARGS, 1024>(
nullptr, 0, "orch_fn", fake_orch_so, sizeof(fake_orch_so), child_ids, children, 2, "cfg_name"
nullptr, 0, 0, "orch_fn", fake_orch_so, sizeof(fake_orch_so), child_ids, children, 2, "cfg_name"
);
}

Expand Down
4 changes: 2 additions & 2 deletions tests/ut/cpp/types/test_chip_max_tensor_args.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ TEST(ChipMaxTensorArgs, ChipStorageHoldsCapacity) {
TEST(ChipMaxTensorArgs, ChipCallableAcceptsCapacity) {
std::vector<ArgDirection> signature(256, ArgDirection::IN);
auto buffer = make_callable<CoreCallable, CHIP_MAX_TENSOR_ARGS, 1024>(
signature.data(), static_cast<int32_t>(signature.size()), "composed", nullptr, 0, nullptr, nullptr, 0, ""
signature.data(), static_cast<int32_t>(signature.size()), 0, "composed", nullptr, 0, nullptr, nullptr, 0, ""
);

const auto &callable = *reinterpret_cast<const ChipCallable *>(buffer.data());
Expand All @@ -63,7 +63,7 @@ TEST(ChipMaxTensorArgs, ChipCallableOverflowReportsRequestedAndSupportedCounts)

try {
(void)make_callable<CoreCallable, CHIP_MAX_TENSOR_ARGS, 1024>(
signature.data(), requested, "overflow", nullptr, 0, nullptr, nullptr, 0, ""
signature.data(), requested, 0, "overflow", nullptr, 0, nullptr, nullptr, 0, ""
);
FAIL() << "expected signature capacity validation to fail";
} catch (const std::invalid_argument &error) {
Expand Down
42 changes: 42 additions & 0 deletions tests/ut/py/test_task_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -1249,6 +1249,48 @@ def test_repr(self):
assert "sig_count=2" in r
assert "child_count=1" in r

def test_scalar_count_defaults_to_zero(self):
chip = ChipCallable.build(
signature=[ArgDirection.IN],
func_name="test_func",
binary=b"\x00",
children=[],
)
assert chip.scalar_count == 0

def test_scalar_count_keyword_round_trip(self):
chip = ChipCallable.build(
signature=[ArgDirection.IN, ArgDirection.OUT],
func_name="test_func",
binary=b"\x00",
children=[],
scalar_count=5,
)
assert chip.scalar_count == 5

def test_scalar_count_survives_from_bytes(self):
chip = ChipCallable.build(
signature=[ArgDirection.IN],
func_name="test_func",
binary=b"\x00",
children=[],
scalar_count=7,
)
raw = ctypes.string_at(int(chip.buffer_ptr()), int(chip.buffer_size()))
clone = ChipCallable.from_bytes(raw)
assert clone.scalar_count == 7

def test_scalar_count_out_of_range_reports_value_and_limit(self):
for bad in (-1, 129):
with pytest.raises(ValueError, match=rf"{bad}.*128"):
ChipCallable.build(
signature=[],
func_name="test_func",
binary=b"\x00",
children=[],
scalar_count=bad,
)


# ============================================================================
# ChipTensor.child_memory
Expand Down
Loading