Skip to content
Merged
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
4 changes: 4 additions & 0 deletions projects/hipdnn/docs/AddingNewOperations.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,10 @@ the right one. Both mistakes compile:
A name is not evidence: `PointwiseAttributes.axis_tensor_uid` is an axis index despite the
suffix, and is correctly unannotated.

The schema must contain exactly one domain-shaped vector: a root vector of tables whose element
carries one integer `cache_uid_key`. The generator refuses to emit when a second candidate
exists, because ordinals would have no defined answer.

### Adding a field

1. Annotate it, or deliberately leave it unannotated.
Expand Down
2 changes: 1 addition & 1 deletion projects/hipdnn/docs/Environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ export HIPDNN_FORCE_BENCHMARKING=0
```

**Notes:**
- With benchmarking on, the first `execute()` of a plan samples every knob-filtered candidate kernel before caching the winner for the plan's life, so it is slower than subsequent calls.
- With benchmarking on and no matching ranking cached, the first `execute()` of a plan samples every knob-filtered candidate kernel, so it is slower than subsequent calls. The ingestor memoizes the ranking per (graph content, device); it survives the plan and persists across processes when disk caching is enabled.
- The variable is process-wide, with no per-provider or per-engine granularity: a leaked value from one test or shell changes an unrelated run.
- `HIPDNN_FORCE_BENCHMARKING=0` also defeats `Graph::autotune()` in EXHAUSTIVE mode, which otherwise sets `global.benchmarking=1` on its priming plans.
- Sampling executes each candidate against the buffers you passed in, so benchmarking assumes idempotent execution or separate input and output buffers -- the same assumption `autotune()` documents. A graph whose output tensor is also one of its inputs is recomputed in place once per sample. The winner runs last, so the final contents are correct, but the buffer is written many times before that.
Expand Down
4 changes: 2 additions & 2 deletions projects/hipdnn/docs/PluginDevelopment.md
Original file line number Diff line number Diff line change
Expand Up @@ -412,11 +412,11 @@ public:

#### Implementing `global.benchmarking`

The generic kernel ingestor engine (`hipdnn_plugin_sdk::ingestor`) implements the global benchmarking knob: setting `global.benchmarking=1` makes it build one candidate plan per knob-filtered catalog entry, time each on the device on first `execute()`, and reuse the fastest for the plan's remaining life. Per-field knobs plus `add_engine_sweep()` remain the way to sweep one metadata dimension and reach every kernel a declared field distinguishes; the two mechanisms are complementary. A provider adopting the ingestor needs no benchmarking code of its own: carry one `hipdnn_plugin_sdk::ingestor::IngestorSettings ingestorSettings` member on its `TSettings` type and a handle exposing `hipStream_t getStream() const`, and `GenericPlanBuilder` handles advertisement, knob reading, the `HIPDNN_FORCE_BENCHMARKING` override, and the composite plan.
The generic kernel ingestor engine (`hipdnn_plugin_sdk::ingestor`) implements the global benchmarking knob: setting `global.benchmarking=1` makes it build one candidate plan per knob-filtered catalog entry, time each on the device on first `execute()`, and memoize the ranking per (graph content, device). The ranking survives the plan that measured it, and persists across processes when disk caching is enabled. Per-field knobs plus `add_engine_sweep()` remain the way to sweep one metadata dimension and reach every kernel a declared field distinguishes; the two mechanisms are complementary. A provider adopting the ingestor needs no benchmarking code of its own: carry one `hipdnn_plugin_sdk::ingestor::IngestorSettings ingestorSettings` member on its `TSettings` type and a handle exposing `hipStream_t getStream() const`, and `GenericPlanBuilder` handles advertisement, knob reading, the `HIPDNN_FORCE_BENCHMARKING` override, and the composite plan.

A handle used with the ingestor **must** expose `hipStream_t getStream() const`: benchmarking's default timer brackets each candidate with HIP events on that stream. Both `GenericPlanBuilder` and `BenchmarkPlan` `static_assert` the requirement, so a handle without it fails to compile with a message naming it rather than failing at plan-build time. The requirement sits on the ingestor templates, not on `validateHandleType()`, so a provider that never instantiates them is unaffected. `BenchmarkPlan` accepts an optional `Timer` in place of that default; it exists so tests can prove candidate selection without a device, and is not part of any stability promise.

`Graph::autotune()` in `EXHAUSTIVE` mode primes every engine advertising the knob: it compiles a *separate* plan carrying `global.benchmarking=1`, executes it once, then discards that plan and compiles and times the plan it keeps from the original knob settings. Discarding is required, because a primed plan re-samples every candidate on each execute and timing it would measure the sampling sweep instead of the kernel. Priming therefore only pays off if its result survives in the provider: MIOpen's search populates a find-db that outlives the throwaway plan. The ingestor keeps its winner only in the plan that measured it, so an `EXHAUSTIVE` run pays `candidates x (warmup + iterations) + 1` extra device executions per spec and then selects by ranked order. `HIPDNN_FORCE_BENCHMARKING=0` suppresses that cost.
`Graph::autotune()` in `EXHAUSTIVE` mode primes every engine advertising the knob: it compiles a *separate* plan carrying `global.benchmarking=1`, executes it once, then discards that plan and compiles and times the plan it keeps from the original knob settings. Discarding is required, because that first execute samples every candidate, and timing it would measure the sampling sweep instead of a single kernel; the ingestor's `BenchmarkPlan` resolves once for the plan's lifetime (see `TheWinnerIsResolvedOnceAcrossRepeatedExecuteCalls`), so this is specifically about the priming execute, not a claim that a benchmarking-enabled plan resamples on every call. Priming therefore only pays off if its result survives in the provider: MIOpen's search populates a find-db that outlives the throwaway plan, and the ingestor's winner cache does the same. The ingestor keeps the measured ranking in its winner cache, keyed by (graph content, device), so the ranking survives the throwaway priming plan. An `EXHAUS…

A provider implementing `global.benchmarking` by hand rather than through the ingestor must apply `hipdnn_plugin_sdk::benchmarkingOverrideFromEnv()` as the ingestor and MIOpen do: consulted *outside* any config-validity branch and composed as `override.value_or(knobValue)` -- **never** an OR. An OR cannot express force-off, because a `false` override term never clears a `true` knob term, so `HIPDNN_FORCE_BENCHMARKING=0` could not override a knob-enabled run.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#pragma once

#include <cstdint>
#include <cstring>
#include <string_view>
#include <type_traits>

Expand All @@ -33,7 +34,7 @@ class Hasher
{
static_assert(std::is_trivially_copyable_v<TValue>);
uint8_t bytes[sizeof(TValue)];
__builtin_memcpy(bytes, &value, sizeof(TValue));
std::memcpy(bytes, &value, sizeof(TValue));
for(uint8_t byte : bytes)
{
tag(byte);
Expand Down Expand Up @@ -413,9 +414,13 @@ inline bool logicallyEqual(const BFloat16Value* a, const BFloat16Value* b, const
{
return false;
}
if(a->value() != b->value())
{
return false;
const auto aValue = a->value();
const auto bValue = b->value();
if(std::memcmp(&aValue, &bValue, sizeof(aValue)) != 0)
{
return false;
}
}
return true;
}
Expand Down Expand Up @@ -1625,9 +1630,13 @@ inline bool logicallyEqual(const Float16Value* a, const Float16Value* b, const U
{
return false;
}
if(a->value() != b->value())
{
return false;
const auto aValue = a->value();
const auto bValue = b->value();
if(std::memcmp(&aValue, &bValue, sizeof(aValue)) != 0)
{
return false;
}
}
return true;
}
Expand All @@ -1653,9 +1662,13 @@ inline bool logicallyEqual(const Float32Value* a, const Float32Value* b, const U
{
return false;
}
if(a->value() != b->value())
{
return false;
const auto aValue = a->value();
const auto bValue = b->value();
if(std::memcmp(&aValue, &bValue, sizeof(aValue)) != 0)
{
return false;
}
}
return true;
}
Expand All @@ -1681,9 +1694,13 @@ inline bool logicallyEqual(const Float64Value* a, const Float64Value* b, const U
{
return false;
}
if(a->value() != b->value())
{
return false;
const auto aValue = a->value();
const auto bValue = b->value();
if(std::memcmp(&aValue, &bValue, sizeof(aValue)) != 0)
{
return false;
}
}
return true;
}
Expand Down Expand Up @@ -2371,17 +2388,41 @@ inline bool logicallyEqual(const PointwiseAttributes* a, const PointwiseAttribut
{
return false;
}
if(a->relu_lower_clip() != b->relu_lower_clip())
{
return false;
const auto aValue = a->relu_lower_clip();
const auto bValue = b->relu_lower_clip();
if(aValue.has_value() != bValue.has_value())
{
return false;
}
if(aValue.has_value() && std::memcmp(&*aValue, &*bValue, sizeof(*aValue)) != 0)
{
return false;
}
}
if(a->relu_upper_clip() != b->relu_upper_clip())
{
return false;
const auto aValue = a->relu_upper_clip();
const auto bValue = b->relu_upper_clip();
if(aValue.has_value() != bValue.has_value())
{
return false;
}
if(aValue.has_value() && std::memcmp(&*aValue, &*bValue, sizeof(*aValue)) != 0)
{
return false;
}
}
if(a->relu_lower_clip_slope() != b->relu_lower_clip_slope())
{
return false;
const auto aValue = a->relu_lower_clip_slope();
const auto bValue = b->relu_lower_clip_slope();
if(aValue.has_value() != bValue.has_value())
{
return false;
}
if(aValue.has_value() && std::memcmp(&*aValue, &*bValue, sizeof(*aValue)) != 0)
{
return false;
}
}
if(a->axis_tensor_uid() != b->axis_tensor_uid())
{
Expand Down Expand Up @@ -2419,17 +2460,41 @@ inline bool logicallyEqual(const PointwiseAttributes* a, const PointwiseAttribut
{
return false;
}
if(a->swish_beta() != b->swish_beta())
{
return false;
const auto aValue = a->swish_beta();
const auto bValue = b->swish_beta();
if(aValue.has_value() != bValue.has_value())
{
return false;
}
if(aValue.has_value() && std::memcmp(&*aValue, &*bValue, sizeof(*aValue)) != 0)
{
return false;
}
}
if(a->elu_alpha() != b->elu_alpha())
{
return false;
const auto aValue = a->elu_alpha();
const auto bValue = b->elu_alpha();
if(aValue.has_value() != bValue.has_value())
{
return false;
}
if(aValue.has_value() && std::memcmp(&*aValue, &*bValue, sizeof(*aValue)) != 0)
{
return false;
}
}
if(a->softplus_beta() != b->softplus_beta())
{
return false;
const auto aValue = a->softplus_beta();
const auto bValue = b->softplus_beta();
if(aValue.has_value() != bValue.has_value())
{
return false;
}
if(aValue.has_value() && std::memcmp(&*aValue, &*bValue, sizeof(*aValue)) != 0)
{
return false;
}
}
return true;
}
Expand Down Expand Up @@ -3587,13 +3652,29 @@ inline bool logicallyEqual(const SdpaAttributes* a, const SdpaAttributes* b, con
{
return false;
}
if(a->dropout_probability() != b->dropout_probability())
{
return false;
const auto aValue = a->dropout_probability();
const auto bValue = b->dropout_probability();
if(aValue.has_value() != bValue.has_value())
{
return false;
}
if(aValue.has_value() && std::memcmp(&*aValue, &*bValue, sizeof(*aValue)) != 0)
{
return false;
}
}
if(a->attn_scale_value() != b->attn_scale_value())
{
return false;
const auto aValue = a->attn_scale_value();
const auto bValue = b->attn_scale_value();
if(aValue.has_value() != bValue.has_value())
{
return false;
}
if(aValue.has_value() && std::memcmp(&*aValue, &*bValue, sizeof(*aValue)) != 0)
{
return false;
}
}
if(a->left_bound() != b->left_bound())
{
Expand Down Expand Up @@ -3940,13 +4021,29 @@ inline bool logicallyEqual(const SdpaBackwardAttributes* a, const SdpaBackwardAt
{
return false;
}
if(a->dropout_probability() != b->dropout_probability())
{
return false;
const auto aValue = a->dropout_probability();
const auto bValue = b->dropout_probability();
if(aValue.has_value() != bValue.has_value())
{
return false;
}
if(aValue.has_value() && std::memcmp(&*aValue, &*bValue, sizeof(*aValue)) != 0)
{
return false;
}
}
if(a->attn_scale_value() != b->attn_scale_value())
{
return false;
const auto aValue = a->attn_scale_value();
const auto bValue = b->attn_scale_value();
if(aValue.has_value() != bValue.has_value())
{
return false;
}
if(aValue.has_value() && std::memcmp(&*aValue, &*bValue, sizeof(*aValue)) != 0)
{
return false;
}
}
if(a->left_bound() != b->left_bound())
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ namespace hipdnn_flatbuffers_sdk::flatbuffer_utilities
///
/// Tensors and nodes compare in vector order, fixed by `IGraph`'s topological-order
/// precondition: two construction orders of one logical DAG miss rather than mismatch.
///
/// A key is usable as an unordered-container key only when `isUsable()` is true. Callers
/// must check that precondition before lookup or insertion: an unusable key is
/// intentionally non-reflexive, so an inserted unusable key can never be found again.
class GraphContentKey
{
public:
Expand Down Expand Up @@ -291,6 +295,16 @@ class GraphContentKey
{
return nullptr;
}

// `IGraph::bytes()`'s validity precondition is documented but unenforced, and
// `root()` calls GetRoot on this copy. Verifying here costs one pass over bytes
// this method already copies, and a malformed buffer becomes an unusable key.
::flatbuffers::Verifier verifier(bytes.data, bytes.size);
if(!hipdnn_flatbuffers_sdk::data_objects::VerifyGraphBuffer(verifier))
{
return nullptr;
}

return std::make_shared<const std::vector<uint8_t>>(bytes.data, bytes.data + bytes.size);
}

Expand All @@ -300,7 +314,7 @@ class GraphContentKey
{
return nullptr;
}
// Verified by GraphWrapper before bytes() would hand them over.
// Verified in retain() before the copy this reads.
return ::flatbuffers::GetRoot<hipdnn_flatbuffers_sdk::data_objects::Graph>(
_content->data());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ class IGraph
getTensorMap() const
= 0;

// ABI rule: `bytes()` is the last virtual in `IGraph`. Appending after it preserves
// existing caller slots, but separately compiled derived classes are not ABI-safe;
// never insert a virtual before it, or existing vtable slots can change.

/// The verified buffer this graph is a view over; the caller does not own it and
/// may copy it to outlive the caller's storage.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ class ContentCarryingTestGraph : public hipdnn_flatbuffers_sdk::flatbuffer_utili
std::vector<int64_t> dims{4, 8};
std::vector<int64_t> strides{8, 1};
std::optional<int64_t> raggedOffsetTensorUid = std::nullopt;
/// A required (non-optional) `value: TensorValue` union, so tests can prove the
/// scalar Float memcmp site folds every byte, not just presence/absence.
std::optional<float> value = std::nullopt;
};
struct NodeSpec
{
Expand All @@ -49,6 +52,8 @@ class ContentCarryingTestGraph : public hipdnn_flatbuffers_sdk::flatbuffer_utili
int64_t in0TensorUid = 1;
int64_t out0TensorUid = 2;
std::optional<int64_t> in1TensorUid = std::nullopt;
/// A real float knob, so tests can prove float fields key on their bytes.
std::optional<float> reluLowerClip = std::nullopt;
};

/// Describes one valid two-tensor, one-node graph by default.
Expand Down Expand Up @@ -144,13 +149,19 @@ class ContentCarryingTestGraph : public hipdnn_flatbuffers_sdk::flatbuffer_utili
void build()
{
using namespace hipdnn_flatbuffers_sdk::data_objects;

std::vector<flatbuffers::Offset<TensorAttributes>> tensorOffsets;
tensorOffsets.reserve(_spec.tensors.size());

for(const auto& tensor : _spec.tensors)
{
auto dims = _builder.CreateVector(tensor.dims);
auto strides = _builder.CreateVector(tensor.strides);
flatbuffers::Offset<void> value = 0;
if(tensor.value.has_value())
{
const Float32Value floatValue(*tensor.value);
value = _builder.CreateStruct(floatValue).Union();
}
TensorAttributesBuilder tensorBuilder(_builder);
tensorBuilder.add_uid(tensor.uid);
tensorBuilder.add_data_type(tensor.dataType);
Expand All @@ -160,6 +171,11 @@ class ContentCarryingTestGraph : public hipdnn_flatbuffers_sdk::flatbuffer_utili
{
tensorBuilder.add_ragged_offset_tensor_uid(*tensor.raggedOffsetTensorUid);
}
if(tensor.value.has_value())
{
tensorBuilder.add_value_type(TensorValue::Float32Value);
tensorBuilder.add_value(value);
}
tensorOffsets.push_back(tensorBuilder.Finish());
}

Expand All @@ -175,6 +191,10 @@ class ContentCarryingTestGraph : public hipdnn_flatbuffers_sdk::flatbuffer_utili
{
attributesBuilder.add_in_1_tensor_uid(*node.in1TensorUid);
}
if(node.reluLowerClip.has_value())
{
attributesBuilder.add_relu_lower_clip(*node.reluLowerClip);
}
const auto attributes = attributesBuilder.Finish();

auto nodeName = _builder.CreateString(node.name);
Expand Down
Loading
Loading