From 2f5e737fa148a2a5fc9a51bae703857c59450b08 Mon Sep 17 00:00:00 2001 From: "KOKO\\Mimi" Date: Sun, 9 Aug 2026 18:52:35 +0900 Subject: [PATCH] feat(linear-static-3d-euler-beam): step 17 - parallel-for-tbb --- ...tic-3d-euler-beam-implementation-report.md | 51 ++++++++++ include/fesa/assembly/parallel_for.hpp | 32 +++++++ src/fesa/CMakeLists.txt | 2 + src/fesa/assembly/parallel_for.cpp | 30 ++++++ tests/CMakeLists.txt | 4 + tests/unit/assembly/parallel_for_test.cpp | 92 +++++++++++++++++++ 6 files changed, 211 insertions(+) create mode 100644 include/fesa/assembly/parallel_for.hpp create mode 100644 src/fesa/assembly/parallel_for.cpp create mode 100644 tests/unit/assembly/parallel_for_test.cpp diff --git a/docs/implementation-plans/linear-static-3d-euler-beam-implementation-report.md b/docs/implementation-plans/linear-static-3d-euler-beam-implementation-report.md index d094810..0bd163b 100644 --- a/docs/implementation-plans/linear-static-3d-euler-beam-implementation-report.md +++ b/docs/implementation-plans/linear-static-3d-euler-beam-implementation-report.md @@ -630,3 +630,54 @@ | FIX1-VERIFY-discovery | `ctest --test-dir .harness/build -C Debug --show-only=json-v1` | 0 | 39 total tests discovered, including the same ten EulerBeam3D names | | FIX1-VERIFY-full | `ctest --test-dir .harness/build -C Debug --output-on-failure` | 0 | 39/39 passed | | FIX1-VERIFY-scans | fail-on-match numerical invariant, dependency, scope, exact-test-count, reference, phase-index, and diff/whitespace scans | 0 | derived rigidity/coefficient and fail-closed checks present; tests 10; leaks 0; reference and phase index unchanged; diff clean | + +## Step 17 — parallel-for-tbb + +- task_id: `TASK-17` +- status: `completed` +- changed_files: `include/fesa/assembly/parallel_for.hpp`, + `src/fesa/assembly/parallel_for.cpp`, + `tests/unit/assembly/parallel_for_test.cpp`, `src/fesa/CMakeLists.txt`, + `tests/CMakeLists.txt`, + `docs/implementation-plans/linear-static-3d-euler-beam-implementation-report.md`, + `phases/linear-static-3d-euler-beam/index.json`, + `.superpowers/sdd/linear-static-3d-euler-beam/task-17-report.md` +- requirement_ids: `FESA-REQ-LS3DEB-024`, `FESA-REQ-LS3DEB-025`, + `FESA-REQ-LS3DEB-034` +- test_ids: `T17-PFOR-001`, `T17-PFOR-002`, `T17-PFOR-003` + +| stage | exact command | exit_code | expected_or_observed_result | evidence_tail | +| --- | --- | ---: | --- | --- | +| RED-build | `cmake --build .harness/build --config Debug --target fesa_tests` | 1 | Exactly three planned tests were registered before production and the ParallelFor public API was absent | MSVC C1083 reported missing `fesa/assembly/parallel_for.hpp` from `parallel_for_test.cpp` after successful CMake regeneration | +| GREEN-build | `cmake --build .harness/build --config Debug --target fesa_tests` | 0 | Minimal serial/oneTBB adapters, three tests, solver library, and unit executable compile and link | `parallel_for.cpp`, `parallel_for_test.cpp`, `fesa_solver.lib`, and `fesa_unit_tests.exe` built without a FESA warning under `/W4 /WX`; oneTBB runtime staged for discovery | +| GREEN-test | `ctest --test-dir .harness/build -C Debug -R ParallelFor --output-on-failure` | 0 | Zero/one/many exact-once, stable indexed output, and exception propagation pass | 3/3 exact `ParallelFor` tests passed | +| VERIFY-configure | `cmake -S . -B .harness/build -A x64 -DFESA_GTEST_SOURCE_DIR=C:/git/googletest "-DMKL_DIR=C:/Program Files (x86)/Intel/oneAPI/mkl/2026.1/lib/cmake/mkl" "-DTBB_DIR=C:/Program Files (x86)/Intel/oneAPI/tbb/2023.1/lib/cmake/tbb" "-DHDF5_DIR=C:/Program Files/HDF_Group/HDF5/2.1.1/cmake"` | 0 | Approved explicit-dependency MSVC x64 build tree generates | Windows SDK, oneMKL 2026.1, oneTBB, and HDF5 resolved; configure and generate completed | +| VERIFY-build | `cmake --build .harness/build --config Debug` | 0 | Full Debug build passes without a new FESA warning | `fesa_solver.lib` and `fesa_unit_tests.exe` built under `/W4 /WX` | +| VERIFY-targeted | `ctest --test-dir .harness/build -C Debug -R ParallelFor --output-on-failure` | 0 | Focused Step 17 suite remains green | 3/3 exact `ParallelFor` tests passed | +| VERIFY-discovery | `ctest --test-dir .harness/build -C Debug --show-only=json-v1` | 0 | CTest discovers the accumulated suite and all three exact ParallelFor names | 42 tests discovered, including 3 `ParallelFor` tests, with feature/unit labels | +| VERIFY-full | `ctest --test-dir .harness/build -C Debug --output-on-failure` | 0 | Full accumulated C++ suite has zero failures | 42/42 tests passed | +| VERIFY-contract-scans | Public oneTBB leak, private include, forbidden scheduler/assembly scope, exact-test-count, and CMake-registration scans using fail-on-match/count `rg` wrappers | 0 | The backend-free adapter implements only independent indexed work and retains the default caller-scoped scheduler policy | public TBB leaks 0; forbidden global/sparse/reduction policy 0; private TBB includes 1; tests 3; registrations 1/1/1 | +| VERIFY-diff | `git diff --check` plus trailing-whitespace scan over the three new files | 0 | Tracked and untracked Step 17 files have no whitespace errors | Diff check exit 0; new-file trailing whitespace matches 0 | +| VERIFY-reference | `git diff --exit-code -- reference/`; `git status --short -- reference/` | 0 | Approved legacy reference artifacts remain read-only and unchanged | Reference diff exit 0 and reference status empty | + +- contract_checks: `ParallelFor`, `SerialParallelFor`, and `TbbParallelFor` + expose only `std::size_t` and `std::function` in the public header. oneTBB + headers/types remain private to `parallel_for.cpp`, and the normalized + `Fesa::TBB` dependency is private to `fesa_solver`. +- contract_checks: both implementations execute `[0,count)` exactly once and + make `count==0` a no-op. Deterministic result ordering is caller-owned: + bodies write only their index-addressed slot, and this adapter performs no + global sparse write, COO/CSR reduction, mutex, or atomic accumulation. +- contract_checks: serial and oneTBB execution preserve the original body + exception type and message. oneTBB cancellation may allow already-running + indexed work to finish; callers receive the rethrown exception and must not + infer a deterministic partial-completion count. +- contract_checks: the adapter uses oneTBB's default caller-scoped scheduler; + it creates no process-wide thread setting, `global_control`, or task arena, + and does not override later MKL/TBB oversubscription coordination. +- generated_evidence: `.harness/build/src/fesa/Debug/fesa_solver.lib`, + `.harness/build/tests/Debug/fesa_unit_tests.exe` +- reference_diff: unchanged; `git diff --exit-code -- reference/` exit 0 +- handoff: Step 18 can inject either backend for independent element-local + computation into caller-owned stable element-order slots before its separate + deterministic COO-to-CSR reduction. diff --git a/include/fesa/assembly/parallel_for.hpp b/include/fesa/assembly/parallel_for.hpp new file mode 100644 index 0000000..59c7402 --- /dev/null +++ b/include/fesa/assembly/parallel_for.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +namespace fesa { + +// Executes independent index-addressed work without exposing the backend. +// Callers own output storage and must confine each invocation to its index. +class ParallelFor { +public: + virtual ~ParallelFor() = default; + virtual void execute( + std::size_t count, + const std::function& body) const = 0; +}; + +class SerialParallelFor final : public ParallelFor { +public: + void execute( + std::size_t count, + const std::function& body) const override; +}; + +class TbbParallelFor final : public ParallelFor { +public: + void execute( + std::size_t count, + const std::function& body) const override; +}; + +} // namespace fesa diff --git a/src/fesa/CMakeLists.txt b/src/fesa/CMakeLists.txt index 2971e04..b8fd5b5 100644 --- a/src/fesa/CMakeLists.txt +++ b/src/fesa/CMakeLists.txt @@ -3,6 +3,7 @@ add_library( STATIC analysis/analysis_model.cpp analysis/analysis_state.cpp + assembly/parallel_for.cpp build_info.cpp core/diagnostic.cpp core/status.cpp @@ -25,6 +26,7 @@ target_link_libraries( fesa_solver PRIVATE Fesa::MKL + Fesa::TBB ) # Product warnings are strict without imposing FESA policy on external targets. diff --git a/src/fesa/assembly/parallel_for.cpp b/src/fesa/assembly/parallel_for.cpp new file mode 100644 index 0000000..7f4a183 --- /dev/null +++ b/src/fesa/assembly/parallel_for.cpp @@ -0,0 +1,30 @@ +#include "fesa/assembly/parallel_for.hpp" + +#include + +namespace fesa { + +void SerialParallelFor::execute( + std::size_t count, + const std::function& body) const { + for (std::size_t index = 0; index < count; ++index) { + body(index); + } +} + +void TbbParallelFor::execute( + std::size_t count, + const std::function& body) const { + if (count == 0U) { + return; + } + + // Use oneTBB's caller-scoped scheduler policy. This adapter does not set + // process-wide concurrency or override the later MKL/TBB oversubscription + // policy. A body exception cancels sibling tasks and is rethrown; work + // already running during cancellation may still finish its indexed slot. + oneapi::tbb::parallel_for( + std::size_t{0}, count, [&body](std::size_t index) { body(index); }); +} + +} // namespace fesa diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 87b9737..4014dad 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -5,6 +5,7 @@ add_executable( unit/build_info_test.cpp unit/analysis/analysis_model_test.cpp unit/analysis/analysis_state_test.cpp + unit/assembly/parallel_for_test.cpp unit/core/diagnostic_test.cpp unit/core/source_identity_test.cpp unit/core/status_test.cpp @@ -45,6 +46,9 @@ if(WIN32) add_custom_command( TARGET fesa_unit_tests POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy_if_different + "$" + "$" COMMAND "${CMAKE_COMMAND}" -E copy_if_different "$" "$" diff --git a/tests/unit/assembly/parallel_for_test.cpp b/tests/unit/assembly/parallel_for_test.cpp new file mode 100644 index 0000000..ee2d57f --- /dev/null +++ b/tests/unit/assembly/parallel_for_test.cpp @@ -0,0 +1,92 @@ +#include "fesa/assembly/parallel_for.hpp" + +#include + +#include +#include +#include +#include +#include +#include + +namespace fesa { +namespace { + +class ParallelForBodyError final : public std::runtime_error { +public: + using std::runtime_error::runtime_error; +}; + +std::array, 2> parallelForBackends( + const SerialParallelFor& serial, + const TbbParallelFor& tbb) { + return {std::cref(serial), std::cref(tbb)}; +} + +TEST(ParallelFor, ZeroOneManyExecuteExactlyOnce) { + const SerialParallelFor serial; + const TbbParallelFor tbb; + + for (const ParallelFor& parallelFor : parallelForBackends(serial, tbb)) { + bool zeroBodyCalled = false; + parallelFor.execute(0U, [&zeroBodyCalled](std::size_t) { + zeroBodyCalled = true; + }); + EXPECT_FALSE(zeroBodyCalled); + + for (const std::size_t count : {1U, 257U}) { + std::vector visits(count, 0); + parallelFor.execute(count, [&visits](std::size_t index) { + ++visits[index]; + }); + EXPECT_EQ(visits, std::vector(count, 1)); + } + } +} + +TEST(ParallelFor, SerialAndTbbProduceStableIndexedOutput) { + constexpr std::size_t count = 1024U; + std::vector serialOutput(count, 0U); + std::vector tbbOutput(count, 0U); + const auto valueForIndex = [](std::size_t index) { + return (index + 17U) * (index + 3U); + }; + + const SerialParallelFor serial; + serial.execute(count, [&serialOutput, &valueForIndex](std::size_t index) { + serialOutput[index] = valueForIndex(index); + }); + + const TbbParallelFor tbb; + tbb.execute(count, [&tbbOutput, &valueForIndex](std::size_t index) { + tbbOutput[index] = valueForIndex(index); + }); + + EXPECT_EQ(tbbOutput, serialOutput); + for (std::size_t index = 0; index < count; ++index) { + EXPECT_EQ(tbbOutput[index], valueForIndex(index)); + } +} + +TEST(ParallelFor, PropagatesBodyExceptionByContract) { + const SerialParallelFor serial; + const TbbParallelFor tbb; + + for (const ParallelFor& parallelFor : parallelForBackends(serial, tbb)) { + try { + // Every iteration throws the same value so the assertion is independent + // of which oneTBB task reports the cancellation-triggering exception. + parallelFor.execute(64U, [](std::size_t) { + throw ParallelForBodyError{"parallel-for-body-failure"}; + }); + ADD_FAILURE() << "ParallelFor swallowed the body exception."; + } catch (const ParallelForBodyError& error) { + EXPECT_EQ(std::string{error.what()}, "parallel-for-body-failure"); + } catch (...) { + ADD_FAILURE() << "ParallelFor changed the body exception type."; + } + } +} + +} // namespace +} // namespace fesa