| | | 1 | | // Copyright 2025 Digital Holography Foundation |
| | | 2 | | // |
| | | 3 | | // Licensed under the Apache License, Version 2.0 (the "License"); |
| | | 4 | | // you may not use this file except in compliance with the License. |
| | | 5 | | // You may obtain a copy of the License at |
| | | 6 | | // |
| | | 7 | | // http://www.apache.org/licenses/LICENSE-2.0 |
| | | 8 | | // |
| | | 9 | | // Unless required by applicable law or agreed to in writing, software |
| | | 10 | | // distributed under the License is distributed on an "AS IS" BASIS, |
| | | 11 | | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| | | 12 | | // See the License for the specific language governing permissions and |
| | | 13 | | // limitations under the License. |
| | | 14 | | |
| | | 15 | | /// @file tasks.hh |
| | | 16 | | /// @brief Interfaces and runtime contexts for Holoflow tasks. |
| | | 17 | | /// |
| | | 18 | | /// A task consumes zero or more input tensors and produces zero or more output |
| | | 19 | | /// tensors. Tasks are either synchronous (blocking) or asynchronous |
| | | 20 | | /// (decoupled push/pop). |
| | | 21 | | /// |
| | | 22 | | /// @section overview Overview |
| | | 23 | | /// - Contexts: @ref holoflow::core::SyncCtx, @ref holoflow::core::AsyncPushCtx, |
| | | 24 | | /// @ref holoflow::core::AsyncPopCtx |
| | | 25 | | /// - Control flow: @ref holoflow::core::OpResult |
| | | 26 | | /// - Task interfaces: @ref holoflow::core::ITask, @ref |
| | | 27 | | /// holoflow::core::ISyncTask, @ref holoflow::core::IAsyncTask |
| | | 28 | | /// - Factories: @ref holoflow::core::ITaskFactory, |
| | | 29 | | /// @ref holoflow::core::ISyncTaskFactory, @ref holoflow::core::IAsyncTaskFactory |
| | | 30 | | /// - Inference result: @ref holoflow::core::InferResult |
| | | 31 | | /// |
| | | 32 | | /// @section lifecycle Lifecycle (single source of truth) |
| | | 33 | | /// - **Inputs**: Scheduler provides views. For owned inputs, it must call |
| | | 34 | | /// @ref holoflow::core::ITask::acquire_input() and place the returned view in |
| | | 35 | | /// the context. |
| | | 36 | | /// - **Execution**: |
| | | 37 | | /// - Sync: scheduler calls @ref holoflow::core::ISyncTask::execute(). |
| | | 38 | | /// - Async push: scheduler calls @ref holoflow::core::IAsyncTask::try_push(). |
| | | 39 | | /// - Async pop: scheduler calls @ref holoflow::core::IAsyncTask::try_pop(). |
| | | 40 | | /// - **Outputs**: Task writes views to the context. For owned outputs the task |
| | | 41 | | /// overwrites slots with owned views. After downstream use, the scheduler |
| | | 42 | | /// must call @ref holoflow::core::ITask::release_output() per owned slot. |
| | | 43 | | /// - **Cancellation**: cooperative via `cancelled`. |
| | | 44 | | /// |
| | | 45 | | /// @section ownership Ownership |
| | | 46 | | /// - Acquire/release apply only to indices marked owned by factory inference. |
| | | 47 | | /// - Calling acquire/release on non-owned indices is undefined. |
| | | 48 | | /// - At most one acquired input group and one unreleased output group exist at |
| | | 49 | | /// a time under SPSC assumptions. |
| | | 50 | | /// - @todo Define policy for discarding an acquired input on cancellation |
| | | 51 | | /// before use (abort API vs implicit discard vs push-then-drop vs aggregation |
| | | 52 | | /// commit). |
| | | 53 | | /// |
| | | 54 | | /// @section errors Errors vs control flow |
| | | 55 | | /// - Exceptions report validation/runtime errors (indices, shapes, allocation, |
| | | 56 | | /// internal failures). |
| | | 57 | | /// - @ref holoflow::core::OpResult is only for control flow: NotReady, |
| | | 58 | | /// Cancelled, Eof. |
| | | 59 | | /// |
| | | 60 | | /// @section concurrency Concurrency |
| | | 61 | | /// - Assumed SPSC for async tasks. MPMC is undefined behavior. |
| | | 62 | | /// - @todo Document/extend semantics if MPMC is added later. |
| | | 63 | | |
| | | 64 | | #pragma once |
| | | 65 | | |
| | | 66 | | #include <atomic> |
| | | 67 | | #include <cuda_runtime.h> |
| | | 68 | | #include <memory> |
| | | 69 | | #include <nlohmann/json.hpp> |
| | | 70 | | #include <optional> |
| | | 71 | | #include <span> |
| | | 72 | | #include <vector> |
| | | 73 | | |
| | | 74 | | #include "driver_types.h" |
| | | 75 | | #include "holoflow/core/tensor.hh" |
| | | 76 | | #include "holoflow_event/router.hh" |
| | | 77 | | |
| | | 78 | | namespace holoflow::core { |
| | | 79 | | |
| | | 80 | | /// Runtime execution context for a synchronous task. |
| | | 81 | | struct SyncCtx { |
| | | 82 | | std::span<TView> inputs; ///< Scheduler-provided input views; some may be owned. |
| | | 83 | | std::span<TView> outputs; ///< Output slots; owned outputs are written by the task. |
| | | 84 | | std::atomic<bool> *cancelled; ///< Non-null cancellation flag. |
| | | 85 | | holoflow_event::EventWriter *event_writer; ///< Event writer for emitting events. |
| | | 86 | | holoflow_event::EventReader *event_reader; ///< Event reader for receiving events. |
| | | 87 | | }; |
| | | 88 | | |
| | | 89 | | /// Runtime execution context for an asynchronous task push operation. |
| | | 90 | | struct AsyncPushCtx { |
| | | 91 | | std::span<TView> inputs; ///< Scheduler-provided input views; some may be owned. |
| | | 92 | | std::atomic<bool> *cancelled; ///< Non-null cancellation flag. |
| | | 93 | | }; |
| | | 94 | | |
| | | 95 | | /// Runtime execution context for an asynchronous task pop operation. |
| | | 96 | | struct AsyncPopCtx { |
| | | 97 | | std::span<TView> outputs; ///< Output slots; owned outputs are written by the task. |
| | | 98 | | std::atomic<bool> *cancelled; ///< Non-null cancellation flag. |
| | | 99 | | }; |
| | | 100 | | |
| | | 101 | | /// Possible expected outcomes of a task operation. |
| | | 102 | | enum class OpResult : uint8_t { |
| | | 103 | | Ok, ///< Completed successfully. |
| | | 104 | | NotReady, ///< Nothing to do now; caller may retry. |
| | | 105 | | Cancelled, ///< Aborted due to cancellation. |
| | | 106 | | Eof, ///< End of stream. |
| | | 107 | | }; |
| | | 108 | | |
| | | 109 | | class IOStorageAccess { |
| | | 110 | | public: |
| | 1 | 111 | | virtual ~IOStorageAccess() = default; |
| | | 112 | | |
| | | 113 | | [[nodiscard]] virtual Storage &owned_input_storage(size_t index) = 0; |
| | | 114 | | [[nodiscard]] virtual Storage &owned_output_storage(size_t index) = 0; |
| | | 115 | | }; |
| | | 116 | | |
| | | 117 | | /// @brief Abstract base interface for tasks with optional tensor ownership. |
| | | 118 | | /// |
| | | 119 | | /// Provides ownership hooks for inputs and outputs. Only indices declared as |
| | | 120 | | /// owned by the task (via factory inference) may be acquired or released. |
| | | 121 | | /// |
| | | 122 | | /// @warning Calling acquire/release on non-owned indices is undefined behavior. |
| | | 123 | | /// @warning Not calling acquire/release on owned indices is undefined behavior. |
| | | 124 | | /// |
| | | 125 | | /// @par Ownership lifecycle |
| | | 126 | | /// - Inputs (owned): |
| | | 127 | | /// - Acquire with @ref acquire_input(int) before execution or push. |
| | | 128 | | /// - Publish the acquired pointer through @ref IOStorageAccess::owned_input_storage. |
| | | 129 | | /// - The acquired view must not be used after the operation returns. |
| | | 130 | | /// - Outputs (owned): |
| | | 131 | | /// - Publish the output pointer through @ref IOStorageAccess::owned_output_storage. |
| | | 132 | | /// - Context TViews share that stable Storage and observe pointer updates directly. |
| | | 133 | | /// - After downstream consumption, scheduler calls @ref release_output(int). |
| | | 134 | | /// - The task controls pointer cleanup and the lifetime of its owned memory. |
| | | 135 | | /// @todo Define rollback semantics on cancellation before use. |
| | | 136 | | class ITask { |
| | | 137 | | public: |
| | 1 | 138 | | virtual ~ITask() = default; |
| | | 139 | | |
| | | 140 | | /// Acquire a writable view for an **owned** input index. |
| | | 141 | | /// Valid only for indices marked owned by inference. |
| | | 142 | | /// The view must reference the compiler-provided Storage for that index. |
| | | 143 | | /// It signals readiness; scheduler contexts already share the same Storage. |
| | | 144 | | /// @returns view or std::nullopt if not currently acquirable. |
| | | 145 | | /// @throws std::out_of_range on bad index. |
| | | 146 | | [[nodiscard]] virtual std::optional<TView> acquire_input(int index); |
| | | 147 | | |
| | | 148 | | /// Release a produced **owned** output at @p index after downstream use. |
| | | 149 | | /// @throws std::out_of_range on bad index. |
| | | 150 | | virtual void release_output(int index); |
| | | 151 | | |
| | | 152 | | void bind_logger(std::shared_ptr<spdlog::logger> logger); |
| | | 153 | | |
| | | 154 | | void bind_storage_access(IOStorageAccess *storage_access); |
| | | 155 | | |
| | | 156 | | protected: |
| | | 157 | | spdlog::logger *logger(); |
| | | 158 | | |
| | | 159 | | [[nodiscard]] IOStorageAccess &storage_access(); |
| | | 160 | | |
| | | 161 | | private: |
| | | 162 | | std::shared_ptr<spdlog::logger> logger_ = nullptr; |
| | | 163 | | IOStorageAccess *storage_access_ = nullptr; |
| | | 164 | | }; |
| | | 165 | | |
| | | 166 | | /// @brief Interface for synchronous (blocking) tasks. |
| | | 167 | | /// |
| | | 168 | | /// A synchronous task consumes inputs and produces outputs within a single |
| | | 169 | | /// blocking call to @ref execute(). |
| | | 170 | | /// |
| | | 171 | | /// @par Synchronous lifecycle |
| | | 172 | | /// 1. Scheduler optionally acquires owned inputs via |
| | | 173 | | /// @ref ITask::acquire_input(int). |
| | | 174 | | /// 2. Scheduler calls @ref execute(). |
| | | 175 | | /// 3. Task may write owned output views into @ref SyncCtx::outputs, |
| | | 176 | | /// overwriting the corresponding slots. |
| | | 177 | | /// 4. Scheduler consumes outputs and calls @ref ITask::release_output(int) |
| | | 178 | | /// for each owned output slot. |
| | | 179 | | /// |
| | | 180 | | /// @note Inputs acquired via @ref ITask::acquire_input(int) must not be used |
| | | 181 | | /// after @ref execute() returns. |
| | | 182 | | class ISyncTask : public ITask { |
| | | 183 | | public: |
| | 1 | 184 | | virtual ~ISyncTask() = default; |
| | | 185 | | |
| | | 186 | | /// Execute in a blocking manner. |
| | | 187 | | /// Overwrites owned output slots in ctx.outputs. |
| | | 188 | | /// @returns control-flow result; errors via exceptions. |
| | | 189 | | [[nodiscard]] virtual OpResult execute(SyncCtx &ctx) = 0; |
| | | 190 | | }; |
| | | 191 | | |
| | | 192 | | /// @brief Interface for asynchronous (decoupled) tasks. |
| | | 193 | | /// |
| | | 194 | | /// Asynchronous tasks split their interaction into a producer-side push and a |
| | | 195 | | /// consumer-side pop. Push submits inputs. Pop retrieves available outputs. |
| | | 196 | | /// |
| | | 197 | | /// @par Push lifecycle (producer) |
| | | 198 | | /// 1. Scheduler optionally acquires owned inputs via |
| | | 199 | | /// @ref ITask::acquire_input(int). |
| | | 200 | | /// 2. Scheduler calls @ref try_push(). |
| | | 201 | | /// 3. Task consumes submitted inputs. |
| | | 202 | | /// |
| | | 203 | | /// @par Pop lifecycle (consumer) |
| | | 204 | | /// 1. Scheduler calls @ref try_pop(). |
| | | 205 | | /// 2. On success, task writes outputs, overwriting slots for owned outputs. |
| | | 206 | | /// 3. Scheduler consumes outputs and calls @ref ITask::release_output(int) for |
| | | 207 | | /// each owned output slot. |
| | | 208 | | class IAsyncTask : public ITask { |
| | | 209 | | public: |
| | 1 | 210 | | virtual ~IAsyncTask() = default; |
| | | 211 | | |
| | | 212 | | /// Producer-side submission. |
| | | 213 | | /// @returns control-flow result; errors via exceptions. |
| | | 214 | | [[nodiscard]] virtual OpResult try_push(AsyncPushCtx &ctx) = 0; |
| | | 215 | | |
| | | 216 | | /// Consumer-side retrieval. |
| | | 217 | | /// Overwrites owned output slots in ctx.outputs. |
| | | 218 | | /// @returns control-flow result; errors via exceptions. |
| | | 219 | | [[nodiscard]] virtual OpResult try_pop(AsyncPopCtx &ctx) = 0; |
| | | 220 | | }; |
| | | 221 | | |
| | | 222 | | /// Describes an in-place link between an input and output tensor. |
| | | 223 | | struct InPlace { |
| | | 224 | | int in_idx; ///< Input index reused/aliased. |
| | | 225 | | int out_idx; ///< Output index reusing input storage. |
| | | 226 | | }; |
| | | 227 | | |
| | | 228 | | /// Kind of task: synchronous or asynchronous. |
| | | 229 | | enum class TaskKind { |
| | | 230 | | Sync, /// Synchronous |
| | | 231 | | Async, /// Asynchronous |
| | | 232 | | }; |
| | | 233 | | |
| | | 234 | | /// Result of task inference from a factory infer function. This provides |
| | | 235 | | /// information about the task's input and output tensor shapes and other |
| | | 236 | | /// properties to the scheduling system / compiler. |
| | | 237 | | struct InferResult { |
| | | 238 | | std::vector<TDesc> input_descs; ///< Inferred input tensor descriptions |
| | | 239 | | std::vector<TDesc> output_descs; ///< Inferred output tensor descriptions |
| | | 240 | | std::vector<InPlace> in_place; ///< In-place input-output tensor pairs |
| | | 241 | | std::vector<bool> owned_inputs; ///< Ownership status of input tensors |
| | | 242 | | std::vector<bool> owned_outputs; ///< Ownership status of output tensors |
| | | 243 | | TaskKind kind; ///< Kind of task (sync or async) |
| | | 244 | | /// Async producer capability. When true, try_push synchronizes its producer stream before any |
| | | 245 | | /// result that lets the scheduler advance. NotReady retries need not synchronize. |
| | | 246 | | bool synchronizes_producer_stream = false; |
| | | 247 | | }; |
| | | 248 | | |
| | | 249 | | /// Context for sync task creation. |
| | | 250 | | struct SyncCreateCtx { |
| | | 251 | | cudaStream_t stream = static_cast<cudaStream_t>(0); ///< CUDA stream for task execution |
| | | 252 | | }; |
| | | 253 | | |
| | | 254 | | /// Context for async task creation. |
| | | 255 | | struct AsyncCreateCtx { |
| | | 256 | | /// CUDA streams for producer side. |
| | | 257 | | cudaStream_t producer_stream = static_cast<cudaStream_t>(0); |
| | | 258 | | /// CUDA streams for consumer side. |
| | | 259 | | cudaStream_t consumer_stream = static_cast<cudaStream_t>(0); |
| | | 260 | | }; |
| | | 261 | | |
| | | 262 | | /// Base factory interface. Provides common inference API. |
| | | 263 | | class ITaskFactory { |
| | | 264 | | public: |
| | 1 | 265 | | virtual ~ITaskFactory() = default; |
| | | 266 | | |
| | | 267 | | /// Infer metadata for a task without constructing it. |
| | | 268 | | /// |
| | | 269 | | /// @param input_descs Upstream input tensor descriptors. |
| | | 270 | | /// @param jsettings Task configuration (read-only view). |
| | | 271 | | /// @return Inference result (I/O descs, ownership, in-place). |
| | | 272 | | /// @throws std::invalid_argument on inference failure. |
| | | 273 | | [[nodiscard]] |
| | | 274 | | virtual InferResult infer(std::span<const TDesc> input_descs, |
| | | 275 | | const nlohmann::json &jsettings) const = 0; |
| | | 276 | | }; |
| | | 277 | | |
| | | 278 | | /// Factory for synchronous tasks. |
| | | 279 | | class ISyncTaskFactory : public ITaskFactory { |
| | | 280 | | public: |
| | 1 | 281 | | virtual ~ISyncTaskFactory() = default; |
| | | 282 | | |
| | | 283 | | /// Create a new synchronous task instance. |
| | | 284 | | /// |
| | | 285 | | /// @param input_descs Upstream input tensor descriptors. |
| | | 286 | | /// @param jsettings Task configuration. |
| | | 287 | | /// @param ctx Runtime handles (sync stream). |
| | | 288 | | /// @return New task ready for `ISyncTask::execute()`. |
| | | 289 | | virtual std::unique_ptr<ISyncTask> create(std::span<const TDesc> input_descs, |
| | | 290 | | const nlohmann::json &jsettings, |
| | | 291 | | const SyncCreateCtx &ctx) const = 0; |
| | | 292 | | |
| | | 293 | | /// Update or replace an existing synchronous task. |
| | | 294 | | /// |
| | | 295 | | /// Use when inputs/settings change. Reuse internal allocations if possible. |
| | | 296 | | /// Otherwise, construct a replacement and return it. |
| | | 297 | | /// |
| | | 298 | | /// @param old_task Existing task to reuse or replace. |
| | | 299 | | /// @param input_descs New input descriptors. |
| | | 300 | | /// @param jsettings New configuration. |
| | | 301 | | /// @param ctx Runtime handles (sync stream). |
| | | 302 | | /// @return Updated task. `old_task` must not be used afterwards. |
| | | 303 | | virtual std::unique_ptr<ISyncTask> update(std::unique_ptr<ISyncTask> old_task, |
| | | 304 | | std::span<const TDesc> input_descs, |
| | | 305 | | const nlohmann::json &jsettings, |
| | | 306 | | const SyncCreateCtx &ctx) const; |
| | | 307 | | }; |
| | | 308 | | |
| | | 309 | | /// Factory for asynchronous tasks. |
| | | 310 | | class IAsyncTaskFactory : public ITaskFactory { |
| | | 311 | | public: |
| | 1 | 312 | | virtual ~IAsyncTaskFactory() = default; |
| | | 313 | | |
| | | 314 | | /// Create a new asynchronous task instance. |
| | | 315 | | /// |
| | | 316 | | /// @param input_descs Upstream input tensor descriptors. |
| | | 317 | | /// @param jsettings Task configuration. |
| | | 318 | | /// @param ctx Runtime handles (producer and consumer streams). |
| | | 319 | | /// @return New task ready for `try_push()/try_pop()` |
| | | 320 | | virtual std::unique_ptr<IAsyncTask> create(std::span<const TDesc> input_descs, |
| | | 321 | | const nlohmann::json &jsettings, |
| | | 322 | | const AsyncCreateCtx &ctx) const = 0; |
| | | 323 | | |
| | | 324 | | /// Update or replace an existing asynchronous task. |
| | | 325 | | /// |
| | | 326 | | /// @param old_task Existing task to reuse or replace. |
| | | 327 | | /// @param input_descs New input descriptors. |
| | | 328 | | /// @param jsettings New configuration. |
| | | 329 | | /// @param ctx Runtime handles (producer and consumer streams). |
| | | 330 | | /// @return Updated task. `old_task` must not be used afterwards. |
| | | 331 | | virtual std::unique_ptr<IAsyncTask> update(std::unique_ptr<IAsyncTask> old_task, |
| | | 332 | | std::span<const TDesc> input_descs, |
| | | 333 | | const nlohmann::json &jsettings, |
| | | 334 | | const AsyncCreateCtx &ctx) const; |
| | | 335 | | }; |
| | | 336 | | |
| | | 337 | | } // namespace holoflow::core |