File size: 17,481 Bytes
9422383 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 | #include "fixed_graph.h"
#include <array>
#include <cmath>
#include <cstdio>
#include <initializer_list>
#include <limits>
#include "model_contract.h"
namespace {
// The YOLO26n topology is fixed, but model resolution and class count still
// determine several tensor dimensions. This is the complete shape vocabulary
// needed by that topology; it is deliberately not a general graph IR.
enum class DimensionKind : uint8_t {
kConstant,
kResolutionDivided,
kResolutionDividedSquared,
kClassCount,
kDetectionChannels,
kClassificationWidth,
kDetectionPositions,
};
struct Dimension {
DimensionKind kind;
uint32_t argument = 0;
};
constexpr Dimension Constant(uint32_t value) { return {DimensionKind::kConstant, value}; }
constexpr Dimension ResolutionDivided(uint32_t divisor) {
return {DimensionKind::kResolutionDivided, divisor};
}
constexpr Dimension ResolutionDividedSquared(uint32_t divisor) {
return {DimensionKind::kResolutionDividedSquared, divisor};
}
constexpr Dimension ClassCount() { return {DimensionKind::kClassCount}; }
constexpr Dimension DetectionChannels() { return {DimensionKind::kDetectionChannels}; }
constexpr Dimension ClassificationWidth() { return {DimensionKind::kClassificationWidth}; }
constexpr Dimension DetectionPositions() { return {DimensionKind::kDetectionPositions}; }
enum class ValueType : uint8_t { kFp16, kFp32 };
struct Padding {
Dimension top, right, bottom, left;
};
struct Kernel {
Dimension height, width;
};
struct Stride {
Dimension height, width;
};
struct Dilation {
Dimension height, width;
};
struct Groups {
Dimension count, input_channels, output_channels;
};
bool Failed(const char* operation, GraphId graph, size_t item) {
std::fprintf(stderr, "native graph construction failed: %s graph=%zu item=%zu\n", operation,
GraphIndex(graph), item);
return false;
}
struct GraphValue {
uint32_t id = XNN_INVALID_VALUE_ID;
};
// Each Build* function below uses this helper to construct one of the model's
// nineteen XNNPACK graphs. Begin creates an empty graph, the Define* methods add
// its tensors, methods such as Convolution add operations, and Finish compiles
// the completed graph into the runtime stored in GraphContext.
//
// XNNPACK assigns every tensor a numeric ID. External inputs and outputs use
// the IDs reserved when the graph is created, while XNNPACK chooses IDs for
// intermediate tensors. GraphValue stores the ID returned by XNNPACK so later
// operations can refer to the correct tensor without relying on declaration
// position. Declaration order remains explicit because it affects XNNPACK's
// workspace planning and runtime performance.
class FixedGraphBuilder {
public:
FixedGraphBuilder(const GraphContext& context, GraphId id, uint32_t external_count)
: context_(context), id_(id), external_count_(external_count) {}
FixedGraphBuilder(const FixedGraphBuilder&) = delete;
FixedGraphBuilder& operator=(const FixedGraphBuilder&) = delete;
~FixedGraphBuilder() {
if (subgraph_) xnn_delete_subgraph(subgraph_);
}
bool Begin() {
return xnn_create_subgraph(external_count_, 0, &subgraph_) == xnn_status_success ||
Failed("xnn_create_subgraph", id_, 0);
}
bool DefinitionsValid() const { return definitions_valid_; }
GraphValue DefineInput(ValueType type, std::initializer_list<Dimension> shape,
ExternalValueId id) {
return DefineValue(type, shape, nullptr, id.value, XNN_VALUE_FLAG_EXTERNAL_INPUT);
}
GraphValue DefineOutput(ValueType type, std::initializer_list<Dimension> shape,
ExternalValueId id) {
return DefineValue(type, shape, nullptr, id.value, XNN_VALUE_FLAG_EXTERNAL_OUTPUT);
}
GraphValue DefineIntermediate(ValueType type, std::initializer_list<Dimension> shape) {
return DefineValue(type, shape, nullptr, XNN_INVALID_VALUE_ID, 0);
}
template <typename Slot>
GraphValue DefineWeight(ValueType type, std::initializer_list<Dimension> shape, Slot slot) {
// Exported learned tensors are FP32. Requiring the exact resolved byte size
// prevents a valid slot from being attached to the wrong fixed-graph value.
std::array<size_t, 4> resolved{};
size_t elements = 0;
if (!ResolveShape(shape, &resolved, &elements) || type != ValueType::kFp32) {
definitions_valid_ = false;
return {};
}
if (elements > std::numeric_limits<size_t>::max() / sizeof(float)) {
definitions_valid_ = false;
return {};
}
const void* data = context_.model->Slot(static_cast<uint32_t>(slot), elements * sizeof(float));
if (!data) {
definitions_valid_ = false;
Failed("model weight slot", id_, value_count_);
return {};
}
return DefineResolvedValue(type, shape.size(), resolved, data, XNN_INVALID_VALUE_ID, 0);
}
bool Convolution(GraphValue input, GraphValue filter, GraphValue bias, GraphValue output,
Padding padding, Kernel kernel, Stride stride, Dilation dilation, Groups groups,
uint32_t flags) {
return Operation(
"xnn_define_convolution_2d",
xnn_define_convolution_2d(
subgraph_, Evaluate32(padding.top), Evaluate32(padding.right),
Evaluate32(padding.bottom), Evaluate32(padding.left), Evaluate32(kernel.height),
Evaluate32(kernel.width), Evaluate32(stride.height), Evaluate32(stride.width),
Evaluate32(dilation.height), Evaluate32(dilation.width), Evaluate32(groups.count),
Evaluate32(groups.input_channels), Evaluate32(groups.output_channels), -INFINITY,
INFINITY, Value(input), Value(filter), Value(bias), Value(output), flags));
}
bool DepthwiseConvolution(GraphValue input, GraphValue filter, GraphValue bias, GraphValue output,
Padding padding, Kernel kernel, Stride stride, Dilation dilation,
Groups groups, uint32_t flags) {
const uint32_t input_channels = Evaluate32(groups.input_channels);
const uint32_t output_channels = Evaluate32(groups.output_channels);
if (input_channels == 0 || output_channels % input_channels != 0) return false;
// XNNPACK accepts a depth multiplier here, whereas Groups records the
// total input and output channel counts used by the model definition.
return Operation(
"xnn_define_depthwise_convolution_2d",
xnn_define_depthwise_convolution_2d(
subgraph_, Evaluate32(padding.top), Evaluate32(padding.right),
Evaluate32(padding.bottom), Evaluate32(padding.left), Evaluate32(kernel.height),
Evaluate32(kernel.width), Evaluate32(stride.height), Evaluate32(stride.width),
Evaluate32(dilation.height), Evaluate32(dilation.width),
output_channels / input_channels, Evaluate32(groups.count), -INFINITY, INFINITY,
Value(input), Value(filter), Value(bias), Value(output), flags));
}
bool Silu(GraphValue input, GraphValue output, uint32_t flags) {
return Unary("xnn_define_silu", xnn_unary_silu, input, output, flags);
}
bool Sigmoid(GraphValue input, GraphValue output, uint32_t flags) {
return Unary("xnn_define_sigmoid", xnn_unary_sigmoid, input, output, flags);
}
bool Add(GraphValue left, GraphValue right, GraphValue output, uint32_t flags) {
return Binary("xnn_define_add", xnn_binary_add, left, right, output, flags);
}
bool Subtract(GraphValue left, GraphValue right, GraphValue output, uint32_t flags) {
return Binary("xnn_define_subtract", xnn_binary_subtract, left, right, output, flags);
}
bool Multiply(GraphValue left, GraphValue right, GraphValue output, uint32_t flags) {
return Binary("xnn_define_multiply", xnn_binary_multiply, left, right, output, flags);
}
bool Divide(GraphValue left, GraphValue right, GraphValue output, uint32_t flags) {
return Binary("xnn_define_divide", xnn_binary_divide, left, right, output, flags);
}
bool Concatenate(std::initializer_list<GraphValue> inputs, GraphValue output, Dimension axis,
uint32_t flags) {
std::array<uint32_t, 5> input_ids{};
if (inputs.size() < 2 || inputs.size() > input_ids.size()) return false;
size_t index = 0;
for (GraphValue input : inputs) input_ids[index++] = Value(input);
return Operation("xnn_define_concatenate",
xnn_define_concatenate(subgraph_, static_cast<int32_t>(Evaluate32(axis)),
inputs.size(), input_ids.data(), Value(output), flags));
}
bool Transpose(GraphValue input, GraphValue output, std::initializer_list<Dimension> permutation,
uint32_t flags) {
return StaticShapeOperation("xnn_define_static_transpose", true, input, output, permutation,
flags);
}
bool Reshape(GraphValue input, GraphValue output, std::initializer_list<Dimension> shape,
uint32_t flags) {
return StaticShapeOperation("xnn_define_static_reshape", false, input, output, shape, flags);
}
bool MaxPool(GraphValue input, GraphValue output, Padding padding, Kernel kernel, Stride stride,
Dilation dilation, uint32_t flags) {
return Operation(
"xnn_define_max_pooling_2d",
xnn_define_max_pooling_2d(subgraph_, Evaluate32(padding.top), Evaluate32(padding.right),
Evaluate32(padding.bottom), Evaluate32(padding.left),
Evaluate32(kernel.height), Evaluate32(kernel.width),
Evaluate32(stride.height), Evaluate32(stride.width),
Evaluate32(dilation.height), Evaluate32(dilation.width),
-INFINITY, INFINITY, Value(input), Value(output), flags));
}
bool BatchMatrixMultiply(GraphValue left, GraphValue right, GraphValue output, uint32_t flags) {
return Operation("xnn_define_batch_matrix_multiply",
xnn_define_batch_matrix_multiply(subgraph_, Value(left), Value(right),
Value(output), flags));
}
bool Softmax(GraphValue input, GraphValue output, uint32_t flags) {
return Operation("xnn_define_softmax",
xnn_define_softmax(subgraph_, Value(input), Value(output), flags));
}
bool Finish() {
// Runtime creation materializes the operators that retain the shared
// workspace and threadpool. The construction-only subgraph can then go.
if (!definitions_valid_) return false;
const xnn_status status =
xnn_create_runtime_v4(subgraph_, nullptr, context_.workspace, context_.threadpool, 0,
&(*context_.runtimes)[GraphIndex(id_)]);
if (status != xnn_status_success) return Failed("xnn_create_runtime_v4", id_, operation_);
xnn_delete_subgraph(subgraph_);
subgraph_ = nullptr;
return true;
}
private:
size_t Evaluate(Dimension dimension) const {
switch (dimension.kind) {
case DimensionKind::kConstant:
return dimension.argument;
case DimensionKind::kResolutionDivided:
return context_.model->resolution() / dimension.argument;
case DimensionKind::kResolutionDividedSquared: {
const size_t side = context_.model->resolution() / dimension.argument;
return side * side;
}
case DimensionKind::kClassCount:
return context_.model->class_count();
case DimensionKind::kDetectionChannels:
return context_.model->class_count() + yolo26::kBoxCoordinateChannels;
case DimensionKind::kClassificationWidth:
return context_.model->classification_width();
case DimensionKind::kDetectionPositions:
return yolo26::DetectionPositions(context_.model->resolution());
}
return 0;
}
uint32_t Evaluate32(Dimension dimension) const {
return static_cast<uint32_t>(Evaluate(dimension));
}
bool ResolveShape(std::initializer_list<Dimension> dimensions, std::array<size_t, 4>* resolved,
size_t* elements) const {
if (dimensions.size() == 0 || dimensions.size() > resolved->size()) return false;
*elements = 1;
size_t axis = 0;
for (Dimension dimension : dimensions) {
const size_t extent = Evaluate(dimension);
if (extent == 0 || *elements > std::numeric_limits<size_t>::max() / extent) return false;
(*resolved)[axis++] = extent;
*elements *= extent;
}
return true;
}
GraphValue DefineValue(ValueType type, std::initializer_list<Dimension> dimensions,
const void* data, uint32_t external_id, uint32_t flags) {
std::array<size_t, 4> resolved{};
size_t elements = 0;
if (!ResolveShape(dimensions, &resolved, &elements)) {
definitions_valid_ = false;
return {};
}
return DefineResolvedValue(type, dimensions.size(), resolved, data, external_id, flags);
}
GraphValue DefineResolvedValue(ValueType type, size_t rank,
const std::array<size_t, 4>& dimensions, const void* data,
uint32_t external_id, uint32_t flags) {
uint32_t value = XNN_INVALID_VALUE_ID;
const xnn_datatype datatype = type == ValueType::kFp16 ? xnn_datatype_fp16 : xnn_datatype_fp32;
if (xnn_define_tensor_value(subgraph_, datatype, rank, dimensions.data(), data, external_id,
flags, &value) != xnn_status_success) {
definitions_valid_ = false;
Failed("xnn_define_tensor_value", id_, value_count_);
return {};
}
++value_count_;
return {value};
}
uint32_t Value(GraphValue value) const { return value.id; }
bool Operation(const char* name, xnn_status status) {
const size_t operation = operation_++;
return status == xnn_status_success || Failed(name, id_, operation);
}
bool Unary(const char* name, xnn_unary_operator operation, GraphValue input, GraphValue output,
uint32_t flags) {
return Operation(
name, xnn_define_unary(subgraph_, operation, nullptr, Value(input), Value(output), flags));
}
bool Binary(const char* name, xnn_binary_operator operation, GraphValue left, GraphValue right,
GraphValue output, uint32_t flags) {
const xnn_binary_params parameters{-INFINITY, INFINITY};
return Operation(name, xnn_define_binary(subgraph_, operation, ¶meters, Value(left),
Value(right), Value(output), flags));
}
bool StaticShapeOperation(const char* name, bool transpose, GraphValue input, GraphValue output,
std::initializer_list<Dimension> dimensions, uint32_t flags) {
std::array<size_t, 4> resolved{};
if (dimensions.size() == 0 || dimensions.size() > resolved.size()) return false;
size_t index = 0;
for (Dimension dimension : dimensions) resolved[index++] = Evaluate(dimension);
const xnn_status status =
transpose ? xnn_define_static_transpose(subgraph_, dimensions.size(), resolved.data(),
Value(input), Value(output), flags)
: xnn_define_static_reshape(subgraph_, dimensions.size(), resolved.data(),
Value(input), Value(output), flags);
return Operation(name, status);
}
const GraphContext& context_;
GraphId id_;
uint32_t external_count_;
xnn_subgraph_t subgraph_ = nullptr;
size_t value_count_ = 0;
size_t operation_ = 0;
bool definitions_valid_ = true;
};
// These are intentionally private textual fragments: they use the builder and
// shape vocabulary in this anonymous namespace, and no translation unit may
// compile or include them independently. Keeping one fragment per model region
// makes the fixed topology navigable without creating another graph format or
// generated-source path. Each Build* function declares one of the 19 runtime
// partitions. Value declarations retain the complete inventory benchmarked on
// Raspberry Pi 5—including entries not consumed by a node—because declaration
// order affects XNNPACK workspace planning and steady-state latency.
// clang-format off
#include "fixed_weight_slots.inc"
#include "fixed_graph_backbone.inc"
#include "fixed_graph_neck.inc"
#include "fixed_graph_detection.inc"
#include "fixed_graph_projection.inc"
// clang-format on
bool BuildAllFixedGraphs(const GraphContext& context) {
// Keep construction explicit and in GraphId order. Runtime execution follows
// the separate fixed schedule, which begins with the projection partitions.
return BuildBackboneP4ToP5(context) && BuildBackboneP5ToAttention(context) &&
BuildBackboneAttentionQkv(context) && BuildBackboneAttentionScale(context) &&
BuildBackboneAttentionMatrix(context) && BuildBackboneAttentionOutput(context) &&
BuildNeckP4Fusion(context) && BuildNeckP4Block(context) && BuildNeckP3Fusion(context) &&
BuildDetectionP3Path(context) && BuildDetectionP4Path(context) &&
BuildDetectionP5Qkv(context) && BuildDetectionP5AttentionScale(context) &&
BuildDetectionP5AttentionMatrix(context) && BuildDetectionP5AndBoxes(context) &&
BuildClassificationAndDecode(context) && BuildStemToP2(context) && BuildP2ToP3(context) &&
BuildP3ToP4(context);
}
} // namespace
bool BuildFixedGraphs(const GraphContext& context) { return BuildAllFixedGraphs(context); }
|