From 1730a7188ec0d543de2d8e3291538cb6f1a75a96 Mon Sep 17 00:00:00 2001 From: ChaoZheng109 Date: Thu, 20 Aug 2026 18:21:39 -0700 Subject: [PATCH] Add: regression UTs for the Graph boundary scalar pool bound MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bind_graph_topology bounds the boundary scalar pool by GRAPH_MAX_SCALAR_ARGS (64), not the per-task MAX_SCALAR_ARGS (16); the mismatch shipped Definitions the device refused at localize (#1939). No case in CI crosses a boundary wider than 16 scalars any more — the DSv4 HBG case's block decomposition brought every boundary under 16 — so the bound has no scene-level regression coverage. - UT: a pool wider than one task payload (MAX_SCALAR_ARGS + 3) localizes and materializes, and a node reads the pool's last entry; fails against the pre-#1939 check - UT: a pool beyond GRAPH_MAX_SCALAR_ARGS is rejected at localize - Correct the GraphTaskArgs sizing comment in both arch pto_types.h: the boundary ships to the device in the submission image and is read there, so widening it also costs submission-image bytes --- .../host_build_graph/runtime/pto_types.h | 8 +- .../host_build_graph/runtime/pto_types.h | 8 +- tests/ut/cpp/common/test_hbg_graph_cache.cpp | 83 +++++++++++++++++-- 3 files changed, 87 insertions(+), 12 deletions(-) diff --git a/src/a2a3/runtime/host_build_graph/runtime/pto_types.h b/src/a2a3/runtime/host_build_graph/runtime/pto_types.h index 507f8c42a9..ab7de868b7 100644 --- a/src/a2a3/runtime/host_build_graph/runtime/pto_types.h +++ b/src/a2a3/runtime/host_build_graph/runtime/pto_types.h @@ -692,9 +692,11 @@ inline constexpr uint32_t GRAPH_MAX_SCALAR_ARGS = 64; // Boundary arguments of a Graph. Sized independently of CoreTaskArgs because a // Graph boundary never becomes a task payload — graph_reset_outer_payload zeroes -// the outer GRAPH task's tensor_count, and the boundary is read host-side only, -// by graph_boundary_matches and graph_build_submission_image. Widening it costs -// orchestration stack, not PTO2TaskPayload and not the bytes shipped to device. +// the outer GRAPH task's tensor_count. The boundary ships to the device inside +// the submission image; materialize stages each node's own arguments from it, +// bounded per node by CoreTaskArgs capacity, and bind_graph_topology bounds the +// pool itself by these constants. Widening them costs orchestration stack and +// submission-image bytes, not PTO2TaskPayload. using GraphTaskArgs = Arg; // ChipTaskArgs — chip-level entry-arg holding the orchestration entry's diff --git a/src/a5/runtime/host_build_graph/runtime/pto_types.h b/src/a5/runtime/host_build_graph/runtime/pto_types.h index b39f43ffed..fd13e8ce29 100644 --- a/src/a5/runtime/host_build_graph/runtime/pto_types.h +++ b/src/a5/runtime/host_build_graph/runtime/pto_types.h @@ -691,9 +691,11 @@ inline constexpr uint32_t GRAPH_MAX_SCALAR_ARGS = 64; // Boundary arguments of a Graph. Sized independently of CoreTaskArgs because a // Graph boundary never becomes a task payload — graph_reset_outer_payload zeroes -// the outer GRAPH task's tensor_count, and the boundary is read host-side only, -// by graph_boundary_matches and graph_build_submission_image. Widening it costs -// orchestration stack, not PTO2TaskPayload and not the bytes shipped to device. +// the outer GRAPH task's tensor_count. The boundary ships to the device inside +// the submission image; materialize stages each node's own arguments from it, +// bounded per node by CoreTaskArgs capacity, and bind_graph_topology bounds the +// pool itself by these constants. Widening them costs orchestration stack and +// submission-image bytes, not PTO2TaskPayload. using GraphTaskArgs = Arg; // ChipTaskArgs — chip-level entry-arg holding the orchestration entry's diff --git a/tests/ut/cpp/common/test_hbg_graph_cache.cpp b/tests/ut/cpp/common/test_hbg_graph_cache.cpp index e24017521a..c99c3f6ae4 100644 --- a/tests/ut/cpp/common/test_hbg_graph_cache.cpp +++ b/tests/ut/cpp/common/test_hbg_graph_cache.cpp @@ -51,7 +51,8 @@ GraphTensor make_test_tensor(uint64_t address) { return tensor; } -std::vector make_test_definition(uint64_t graph_key, uint64_t boundary_address) { +std::vector +make_test_definition(uint64_t graph_key, uint64_t boundary_address, uint32_t boundary_scalar_count = 1) { std::vector image(sizeof(GraphDefinition)); std::vector fanin_offsets{0, 0, 1}; @@ -86,6 +87,7 @@ std::vector make_test_definition(uint64_t graph_key, uint64_t boundar std::vector scalars{0, 18}; std::vector scalar_sources(2); scalar_sources[0].source = static_cast(GraphScalarSource::BOUNDARY); + scalar_sources[0].source_index = boundary_scalar_count - 1; scalar_sources[1].source = static_cast(GraphScalarSource::STATIC_VALUE); GraphDefinition definition{}; @@ -95,7 +97,7 @@ std::vector make_test_definition(uint64_t graph_key, uint64_t boundar definition.edge_count = 1; definition.root_count = 1; definition.boundary_count = 1; - definition.boundary_scalar_count = 1; + definition.boundary_scalar_count = boundary_scalar_count; definition.tensor_arg_count = 2; definition.scalar_arg_count = 2; definition.off_fanin_offsets = append_section(image, fanin_offsets); @@ -120,13 +122,20 @@ std::vector make_test_definition(uint64_t graph_key, uint64_t boundar return image; } -std::vector make_test_submission(uint64_t graph_key, uint64_t boundary_address, uint64_t boundary_scalar) { +// The pool holds boundary_scalar_count entries; boundary_scalar lands in the +// last one, where make_test_definition's BOUNDARY ref reads it. +std::vector make_test_submission( + uint64_t graph_key, uint64_t boundary_address, uint64_t boundary_scalar, uint32_t boundary_scalar_count = 1 +) { const size_t tensors_offset = PTO2_ALIGN_UP(sizeof(GraphSubmission), alignof(GraphTensor)); const size_t scalars_offset = PTO2_ALIGN_UP(tensors_offset + sizeof(GraphTensor), alignof(uint64_t)); - std::vector image(scalars_offset + sizeof(uint64_t)); + std::vector image(scalars_offset + boundary_scalar_count * sizeof(uint64_t)); const GraphTensor boundary = make_test_tensor(boundary_address); std::memcpy(image.data() + tensors_offset, &boundary, sizeof(boundary)); - std::memcpy(image.data() + scalars_offset, &boundary_scalar, sizeof(boundary_scalar)); + std::memcpy( + image.data() + scalars_offset + (boundary_scalar_count - 1) * sizeof(uint64_t), &boundary_scalar, + sizeof(boundary_scalar) + ); GraphSubmission submission{}; submission.graph_key = graph_key; @@ -134,7 +143,7 @@ std::vector make_test_submission(uint64_t graph_key, uint64_t boundar submission.tensors_offset = static_cast(tensors_offset); submission.tensor_count = 1; submission.scalars_offset = static_cast(scalars_offset); - submission.scalar_count = 1; + submission.scalar_count = boundary_scalar_count; std::memcpy(image.data(), &submission, sizeof(submission)); return image; } @@ -376,6 +385,68 @@ TEST(GraphExecutionReplay, ResubmissionRebuildsFromDefinition) { EXPECT_EQ(node.payload.dump_metadata.dump_arg_mask, uint64_t{1} << 0); } +// The boundary scalar pool is bounded by the Graph boundary contract +// (GRAPH_MAX_SCALAR_ARGS), not by a single task payload's MAX_SCALAR_ARGS — +// a node stages at most MAX_SCALAR_ARGS entries from it, but the pool itself +// may be wider. +TEST(GraphExecutionReplay, LocalizesBoundaryScalarPoolWiderThanTaskPayload) { + constexpr uint64_t GRAPH_KEY_VALUE = 0x1234; + constexpr uint32_t POOL = MAX_SCALAR_ARGS + 3; + static_assert(POOL <= GRAPH_MAX_SCALAR_ARGS); + std::array boundary{}; + + const std::vector definition = + make_test_definition(GRAPH_KEY_VALUE, reinterpret_cast(boundary.data()), POOL); + const TestDefinitionObject definition_object(definition); + OuterHeap heap(definition); + std::vector submission_image = + make_test_submission(GRAPH_KEY_VALUE, reinterpret_cast(boundary.data()), 21, POOL); + auto &submission = *reinterpret_cast(submission_image.data()); + submission.definition_addr = definition_object.address(); + submission.definition_hash = definition_object.hash(); + + PTO2TaskDescriptor outer_task{}; + outer_task.task_id = PTO2TaskId::make(1, 7); + outer_task.packed_buffer_base = heap.base(); + outer_task.packed_buffer_end = heap.end(); + PTO2TaskSlotState outer_slot{}; + outer_slot.task_kind = TaskKind::GRAPH; + outer_slot.task = &outer_task; + outer_slot.graph_context = &submission; + + GraphExecution *execution = graph_execution_localize(outer_slot); + ASSERT_NE(execution, nullptr); + EXPECT_EQ(graph_execution_materialize_slice(outer_slot, *execution, 2), GraphMaterializeResult::PREPARED); + EXPECT_EQ(execution->node_storage[0].payload.scalars[0], 21U); +} + +TEST(GraphExecutionReplay, RejectsBoundaryScalarPoolBeyondContract) { + constexpr uint64_t GRAPH_KEY_VALUE = 0x1234; + constexpr uint32_t POOL = GRAPH_MAX_SCALAR_ARGS + 1; + std::array boundary{}; + + const std::vector definition = + make_test_definition(GRAPH_KEY_VALUE, reinterpret_cast(boundary.data()), POOL); + const TestDefinitionObject definition_object(definition); + OuterHeap heap(definition); + std::vector submission_image = + make_test_submission(GRAPH_KEY_VALUE, reinterpret_cast(boundary.data()), 21, POOL); + auto &submission = *reinterpret_cast(submission_image.data()); + submission.definition_addr = definition_object.address(); + submission.definition_hash = definition_object.hash(); + + PTO2TaskDescriptor outer_task{}; + outer_task.task_id = PTO2TaskId::make(1, 7); + outer_task.packed_buffer_base = heap.base(); + outer_task.packed_buffer_end = heap.end(); + PTO2TaskSlotState outer_slot{}; + outer_slot.task_kind = TaskKind::GRAPH; + outer_slot.task = &outer_task; + outer_slot.graph_context = &submission; + + EXPECT_EQ(graph_execution_localize(outer_slot), nullptr); +} + TEST(GraphDefinitionObject, RejectsDefinitionBeyondRetainedBytes) { constexpr uint64_t GRAPH_KEY_VALUE = 0x4567; std::array boundary{};