perf(shader): memoize SPIR-V emission failures
The source entry remembers a resource plan that failed to build, but not a failed emission: findOrCompileVariant kept only successful variants, and the first attempt used up the prepared program, so every request for a program whose emission throws ran PrepareResourceProgram and the emitter again before the draw was skipped. compileVariant now reports an exception from SpirvEmitter::Emit (and from SPIRV-Tools validation when enabled), and the source entry keeps it per code address, binding layout and resource specialization; the next request for that variant rethrows it before the disk-cache lookup. Descriptor population depends on the snapshot and an out-of-memory error is transient, so neither is kept. Nothing reaches the disk cache, and APS5_NO_FAILURE_MEMO=1 turns the memo off as for plan failures.
This commit is contained in:
parent
ee391a5614
commit
bcdf19ee4e
|
|
@ -1,5 +1,6 @@
|
|||
#include "ShaderDiskCache.hpp"
|
||||
#include "ShaderCacheDirectory.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
|
|
@ -7,6 +8,7 @@
|
|||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <iostream>
|
||||
#include <random>
|
||||
#include <stdexcept>
|
||||
|
|
@ -436,6 +438,96 @@ void verifyAcrossProcesses(const char* self) {
|
|||
require(std::system(command.c_str()) == 0, "the loading process failed");
|
||||
}
|
||||
|
||||
struct ClockRequest {
|
||||
std::vector<std::uint32_t> code{0xf4900100u, 0x00000000u, 0xbf8cc07fu, 0x7e020204u, 0xe0700000u, 0x80000100u, 0xf800180fu, 0x01010101u, 0xbf810000u};
|
||||
std::array<std::uint32_t, 4> userData{0x10000000u, 0x00000000u, 0x40u, 0x00027facu};
|
||||
std::array<std::uint32_t, 1> capabilities{1u};
|
||||
RecompileRequest request{};
|
||||
|
||||
explicit ClockRequest(std::uint32_t stride = 0) {
|
||||
userData[1] = stride << 16u;
|
||||
ShaderPixelStageInfo pixel{};
|
||||
pixel.inputAddr = PixelInputBit(PixelInput::PerspectiveCenter);
|
||||
pixel.hasPerspectiveCenterVgpr = true;
|
||||
pixel.targetOutputMode[0] = 9u;
|
||||
pixel.targetExportMapping.fill(0xe4u);
|
||||
request.shader = {ShaderStage::Fragment, 0x30000u, code, 0, {}};
|
||||
request.context.waveSize = 64;
|
||||
request.context.userDataBaseRegister = 0;
|
||||
request.context.userData = userData;
|
||||
request.context.pixel = pixel;
|
||||
request.target.vulkanVersion = 0x00401000u;
|
||||
request.target.spirvVersion = 0x00010300u;
|
||||
request.target.subgroupSize = 32;
|
||||
request.target.supportedCapabilities = capabilities;
|
||||
request.layout.pushConstantSizeBytes = 128;
|
||||
}
|
||||
};
|
||||
|
||||
std::string emissionFailure(const RecompileRequest& request) {
|
||||
try {
|
||||
static_cast<void>(Recompile(request));
|
||||
} catch (const std::runtime_error& error) {
|
||||
const std::string message = error.what();
|
||||
require(message.find("reads the shader clock, which needs the device's VK_KHR_shader_clock") != std::string::npos, "unexpected failure: " + message.substr(0, message.find("RecompileRequest:")));
|
||||
return message;
|
||||
}
|
||||
throw std::runtime_error("a shader clock read compiled without VK_KHR_shader_clock");
|
||||
}
|
||||
|
||||
std::uint64_t diskMisses() {
|
||||
return ShaderDiskCache::Totals().misses;
|
||||
}
|
||||
|
||||
void verifyEmissionFailureMemo() {
|
||||
const auto before = ShaderDiskCache::Totals();
|
||||
ClockRequest clock;
|
||||
const auto first = emissionFailure(clock.request);
|
||||
require(diskMisses() == before.misses + 1, "the first compile was not looked up");
|
||||
require(emissionFailure(clock.request) == first, "the second compile failed with another message");
|
||||
require(diskMisses() == before.misses + 1, "the second compile looked up and emitted the program again");
|
||||
|
||||
ClockRequest layout;
|
||||
layout.request.layout = {0, 0, 16, 112};
|
||||
static_cast<void>(emissionFailure(layout.request));
|
||||
require(diskMisses() == before.misses + 2, "another binding layout reused the failure");
|
||||
ClockRequest stride(16);
|
||||
static_cast<void>(emissionFailure(stride.request));
|
||||
require(diskMisses() == before.misses + 3, "another buffer specialization reused the failure");
|
||||
ClockRequest relocated;
|
||||
relocated.request.shader.codeAddress += 0x1000u;
|
||||
static_cast<void>(emissionFailure(relocated.request));
|
||||
require(diskMisses() == before.misses + 4, "a relocated copy reused the failure");
|
||||
require(emissionFailure(clock.request) == first && diskMisses() == before.misses + 4, "the other requests replaced the failure");
|
||||
|
||||
ClockRequest concurrent;
|
||||
concurrent.request.layout.pushConstantSizeBytes = 64;
|
||||
std::array<std::future<std::string>, 4> failures;
|
||||
for (auto& future : failures) future = std::async(std::launch::async, [&concurrent] { return emissionFailure(concurrent.request); });
|
||||
std::vector<std::string> messages;
|
||||
for (auto& future : failures) messages.push_back(future.get());
|
||||
require(std::all_of(messages.begin(), messages.end(), [&](const std::string& message) { return message == messages.front(); }), "concurrent requests failed with different messages");
|
||||
require(diskMisses() == before.misses + 5, "concurrent requests emitted the program " + std::to_string(diskMisses() - before.misses - 4) + " times");
|
||||
|
||||
ShaderDiskCache::Flush();
|
||||
require(ShaderDiskCache::Totals().writes == before.writes, "an emission failure was stored in the disk cache");
|
||||
}
|
||||
|
||||
int runWithoutFailureMemo() {
|
||||
ClockRequest clock;
|
||||
const auto first = emissionFailure(clock.request);
|
||||
require(emissionFailure(clock.request) == first, "the second compile failed with another message");
|
||||
require(diskMisses() == 2, "APS5_NO_FAILURE_MEMO=1 did not emit the program again (" + std::to_string(diskMisses()) + " lookups)");
|
||||
std::cout << "APS5_NO_FAILURE_MEMO=1 emitted the failing program twice\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
void verifyFailureMemoSwitch(const char* self) {
|
||||
setEnvironment("APS5_NO_FAILURE_MEMO", "1");
|
||||
const std::string command = "\"" + std::string(self) + "\" --no-failure-memo";
|
||||
require(std::system(command.c_str()) == 0, "the process with APS5_NO_FAILURE_MEMO=1 failed");
|
||||
}
|
||||
|
||||
void verifyDefaultDirectory(const char* self) {
|
||||
setEnvironment("ANYPS5_SHADER_CACHE_DIR", "");
|
||||
setEnvironment("ANYPS5_NO_SHADER_CACHE", "1");
|
||||
|
|
@ -452,6 +544,7 @@ void verifyDefaultDirectory(const char* self) {
|
|||
int main(int argc, char** argv) {
|
||||
try {
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--load") return runLoadingProcess();
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--no-failure-memo") return runWithoutFailureMemo();
|
||||
const auto directory = std::filesystem::temp_directory_path() / ("aps5-shader-disk-cache-test-" + std::to_string(std::random_device{}()));
|
||||
std::filesystem::remove_all(directory);
|
||||
verifyDefaultDirectory(argv[0]);
|
||||
|
|
@ -462,6 +555,8 @@ int main(int argc, char** argv) {
|
|||
verifyKeySensitivity();
|
||||
verifyStore();
|
||||
verifyAcrossProcesses(argv[0]);
|
||||
verifyEmissionFailureMemo();
|
||||
verifyFailureMemoSwitch(argv[0]);
|
||||
std::error_code error;
|
||||
std::filesystem::remove_all(directory, error);
|
||||
std::cout << "shader disk cache tests passed\n";
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
#include "ShaderDiskCache.hpp"
|
||||
#include <list>
|
||||
#include <mutex>
|
||||
#include <new>
|
||||
#include <shared_mutex>
|
||||
#include <unordered_map>
|
||||
#include "ControlFlow/include/ControlFlow/GraphBuilder.hpp"
|
||||
|
|
@ -183,6 +184,13 @@ struct ResultMemoEntry {
|
|||
std::shared_ptr<const RecompileResult> result;
|
||||
};
|
||||
|
||||
struct EmissionFailure {
|
||||
std::uint64_t codeAddress;
|
||||
BindingLayout layout;
|
||||
ResourceSpecialization specialization;
|
||||
std::exception_ptr failure;
|
||||
};
|
||||
|
||||
struct SourceEntry {
|
||||
std::mutex mutex;
|
||||
// The code the entry was built for: the key carries only a hash of it, so a candidate entry is
|
||||
|
|
@ -196,6 +204,7 @@ struct SourceEntry {
|
|||
std::exception_ptr planFailure;
|
||||
std::unique_ptr<IrProgram> program;
|
||||
std::vector<std::shared_ptr<const CompiledVariant>> variants;
|
||||
std::vector<EmissionFailure> emissionFailures;
|
||||
// The result memo, most recently used first, at most ResultMemoEntries (under mutex).
|
||||
std::list<ResultMemoEntry> memo;
|
||||
std::unordered_map<std::uint64_t, std::list<ResultMemoEntry>::iterator> memoIndex;
|
||||
|
|
@ -225,6 +234,11 @@ struct SourceKeyHash {
|
|||
}
|
||||
};
|
||||
|
||||
bool FailureMemo() {
|
||||
static const bool memo = std::getenv("APS5_NO_FAILURE_MEMO") == nullptr;
|
||||
return memo;
|
||||
}
|
||||
|
||||
std::shared_ptr<SourceEntry> getSource(const RecompileRequest& request) {
|
||||
static std::shared_mutex mutex;
|
||||
// Entries whose code hashes alike share a bucket; the code comparison picks the right one.
|
||||
|
|
@ -266,14 +280,13 @@ std::shared_ptr<SourceEntry> getSource(const RecompileRequest& request) {
|
|||
{
|
||||
std::lock_guard lock(source->mutex);
|
||||
if (source->plan == nullptr) {
|
||||
static const bool memoFailures = std::getenv("APS5_NO_FAILURE_MEMO") == nullptr;
|
||||
if (memoFailures && source->planFailure) std::rethrow_exception(source->planFailure);
|
||||
if (FailureMemo() && source->planFailure) std::rethrow_exception(source->planFailure);
|
||||
try {
|
||||
ResourceProgram resource(request);
|
||||
source->plan = std::move(resource.plan);
|
||||
source->program = std::move(resource.program);
|
||||
} catch (...) {
|
||||
if (memoFailures) source->planFailure = std::current_exception();
|
||||
if (FailureMemo()) source->planFailure = std::current_exception();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
|
@ -290,7 +303,7 @@ std::uint64_t nextVariantId() {
|
|||
return variants.fetch_add(1, std::memory_order_relaxed) + 1;
|
||||
}
|
||||
|
||||
CompiledVariant compileVariant(const RecompileRequest& request, IrProgram program, const ResourceSnapshot& resourceSnapshot, const ResourceSpecialization& resourceSpecialization) {
|
||||
CompiledVariant compileVariant(const RecompileRequest& request, IrProgram program, const ResourceSnapshot& resourceSnapshot, const ResourceSpecialization& resourceSpecialization, std::exception_ptr* emissionFailure = nullptr) {
|
||||
const auto inputInfo = RequestInputInfo(request);
|
||||
constexpr DeadCodeEliminator deadCodeEliminator;
|
||||
constexpr ResourceMaterializer resourceMaterializer;
|
||||
|
|
@ -320,11 +333,17 @@ CompiledVariant compileVariant(const RecompileRequest& request, IrProgram progra
|
|||
constexpr SpirvEmitter spirvEmitter;
|
||||
RecompileResult result;
|
||||
result.variantId = nextVariantId();
|
||||
result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions);
|
||||
|
||||
try {
|
||||
result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions);
|
||||
#if ANYPS5_ENABLE_SPIRV_TOOLS
|
||||
result.spirv = ValidateAndOptimizeSpirv(result.spirv, request.target.vulkanVersion, request.target.spirvVersion, request.target.nonConstantImageOffsets);
|
||||
result.spirv = ValidateAndOptimizeSpirv(result.spirv, request.target.vulkanVersion, request.target.spirvVersion, request.target.nonConstantImageOffsets);
|
||||
#endif
|
||||
} catch (const std::bad_alloc&) {
|
||||
throw;
|
||||
} catch (...) {
|
||||
if (emissionFailure != nullptr) *emissionFailure = std::current_exception();
|
||||
throw;
|
||||
}
|
||||
|
||||
result.bdaAbiVersion = program.Info().usesDma ? request.target.bdaAbiVersion : 0u;
|
||||
result.memoryOffsetDword = bindings.layout.memoryOffsetDword;
|
||||
|
|
@ -383,6 +402,9 @@ std::shared_ptr<const CompiledVariant> findOrCompileVariant(SourceEntry& source,
|
|||
return candidate;
|
||||
}
|
||||
}
|
||||
for (const auto& failure : source.emissionFailures) {
|
||||
if (failure.codeAddress == request.shader.codeAddress && sameLayout(failure.layout, request.layout) && failure.specialization == specialization) std::rethrow_exception(failure.failure);
|
||||
}
|
||||
cacheHit = false;
|
||||
const bool disk = ShaderDiskCache::Enabled() && !DebugProbeActive();
|
||||
std::vector<std::byte> diskKey;
|
||||
|
|
@ -400,7 +422,13 @@ std::shared_ptr<const CompiledVariant> findOrCompileVariant(SourceEntry& source,
|
|||
if (variant == nullptr) {
|
||||
auto program = source.program != nullptr ? std::move(*source.program) : PrepareResourceProgram(request);
|
||||
source.program.reset();
|
||||
variant = std::make_shared<const CompiledVariant>(compileVariant(request, std::move(program), snapshot, specialization));
|
||||
std::exception_ptr emissionFailure;
|
||||
try {
|
||||
variant = std::make_shared<const CompiledVariant>(compileVariant(request, std::move(program), snapshot, specialization, &emissionFailure));
|
||||
} catch (...) {
|
||||
if (emissionFailure != nullptr && FailureMemo()) source.emissionFailures.push_back({request.shader.codeAddress, request.layout, specialization, emissionFailure});
|
||||
throw;
|
||||
}
|
||||
if (disk) ShaderDiskCache::Store(std::move(diskKey), variant);
|
||||
}
|
||||
source.program.reset();
|
||||
|
|
|
|||
Loading…
Reference in New Issue