Repository navigation
perf(agc): create compute pipelines before taking the GPU lock - #4897
karanvora2599 wants to merge 1 commit into
Conversation
PrepareDispatch now creates a missing compute pipeline once the descriptor set layout is known, so the driver's compile no longer runs under GuestMemory::GpuMutex. Other queues, presentation and label writes go on during it, and compiles from different queues overlap. Two queues that need the same new variant create it once. The pipeline is still created in dispatch() when the layout is only built under the lock, with APS5_LOCKED_BUILD=1, or with the new APS5_LOCKED_PIPELINE=1.
|
The measurement test behind the Before / after table. It isn't part of this PR. To run it:
Each run builds new programs from a time-based seed, so the pipeline cache never serves them. NvidiaQueueCompileMeasure.cpp#include "VulkanTestDevice.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
#include "prx/libSceAgcDriver/Submit/include/Acb.hpp"
#include <algorithm>
#include <array>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <deque>
#include <initializer_list>
#include <iostream>
#include <stdexcept>
#include <string>
#include <thread>
#include <vector>
namespace {
void Require(bool condition, const std::string& message) {
if (!condition) throw std::runtime_error(message);
}
constexpr std::uint32_t Threads = 32;
constexpr std::size_t CodeWords = 12288;
struct Program {
alignas(256) std::array<std::uint32_t, CodeWords> code{};
};
struct alignas(256) Flag {
volatile std::uint32_t value = 0;
};
std::array<Program, 6> Programs;
std::array<Flag, 8> Flags;
alignas(256) std::array<std::uint32_t, Threads * 4> Input{};
alignas(256) std::array<std::uint32_t, Threads * 16> Output{};
alignas(256) std::array<std::uint32_t, 8> UserData{};
std::uint32_t Low(const volatile void* pointer) {
return static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(pointer));
}
std::uint32_t High(const volatile void* pointer) {
return static_cast<std::uint32_t>(static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(pointer)) >> 32u);
}
std::uint32_t Vop2(std::uint32_t opcode, std::uint32_t dst, std::uint32_t src0, std::uint32_t vsrc1) {
return (opcode << 25) | (dst << 17) | (vsrc1 << 9) | (256u + src0);
}
void BuildIlp(Program& program, std::uint32_t length, std::uint32_t literal) {
std::vector<std::uint32_t> code{0x34020084u, 0x34060086u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u};
code.push_back(0x7e000000u | (9u << 17) | (1u << 9) | 255u);
code.push_back(literal);
for (std::uint32_t chain = 0; chain < 8u; ++chain) code.push_back(Vop2(chain % 2u == 0u ? 0x08u : 0x03u, 10u + chain, 4u + chain % 4u, 9u));
for (std::uint32_t i = 0; i < length; ++i) {
const std::uint32_t dst = 10u + i % 8u;
const std::uint32_t a = 10u + (i + 3u) % 8u;
const std::uint32_t b = 10u + (i + 5u) % 8u;
switch (i % 4u) {
case 0u: code.push_back(Vop2(0x08u, dst, dst, a)); break;
case 1u: code.push_back(Vop2(0x03u, dst, a, dst)); break;
case 2u:
code.push_back(0xd54b0000u | dst);
code.push_back(((256u + b) << 18) | ((256u + a) << 9) | (256u + dst));
break;
default: code.push_back(Vop2(0x04u, dst, dst, b)); break;
}
}
for (std::uint32_t chain = 0; chain < 4u; ++chain) code.push_back(Vop2(0x03u, 10u + chain, 14u + chain, 10u + chain));
code.insert(code.end(), {0xe0781000u, 0x80010a03u, 0xbf810000u});
Require(code.size() <= CodeWords, "program does not fit");
std::copy(code.begin(), code.end(), program.code.begin());
}
void SetShaderRegister(std::vector<std::uint32_t>& words, std::uint32_t offset, std::uint32_t value) {
words.insert(words.end(), {0xc0017600u, offset, value});
}
void Dispatch(std::vector<std::uint32_t>& words, const Program& program) {
const auto address = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(program.code.data()));
SetShaderRegister(words, 0x20c, static_cast<std::uint32_t>(address >> 8u));
SetShaderRegister(words, 0x20d, static_cast<std::uint32_t>(address >> 40u) & 0xffu);
SetShaderRegister(words, 0x207, Threads);
SetShaderRegister(words, 0x208, 1);
SetShaderRegister(words, 0x209, 1);
SetShaderRegister(words, 0x212, 0);
SetShaderRegister(words, 0x213, static_cast<std::uint32_t>(UserData.size()) << 1u);
std::uint32_t offset = 0x240;
for (const auto value : UserData) SetShaderRegister(words, offset++, value);
words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x8041u});
}
void WriteData(std::vector<std::uint32_t>& words, Flag& flag, std::uint32_t value) {
words.insert(words.end(), {0xc0033700u, 0x00100200u, Low(&flag.value), High(&flag.value), value});
}
void Submit(std::uint32_t queue, std::vector<std::uint32_t>& words) {
Packet packet{words.data(), static_cast<std::uint32_t>(words.size()), 0, {}};
Require(sceAgcDriverSubmitAcb(queue, &packet) == 0, "compute submit failed");
}
double Since(std::chrono::steady_clock::time_point start) {
return std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count();
}
double WaitFor(Flag& flag, std::chrono::steady_clock::time_point start) {
while (flag.value == 0) {
Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(180), "a submission did not finish");
std::this_thread::sleep_for(std::chrono::microseconds(200));
}
return Since(start);
}
}
int main() {
try {
auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
device.reset();
for (std::uint32_t lane = 0; lane < Threads; ++lane) {
Input[lane * 4u] = 0x3f800000u + lane;
Input[lane * 4u + 1u] = 0x3f7ff972u;
Input[lane * 4u + 2u] = 0x358637bdu;
Input[lane * 4u + 3u] = 0x3f000000u;
}
UserData = {Low(Input.data()), High(Input.data()) & 0xffffu, static_cast<std::uint32_t>(Input.size() * 4u), 0x31016facu,
Low(Output.data()), High(Output.data()) & 0xffffu, static_cast<std::uint32_t>(Output.size() * 4u), 0x31016facu};
const char* lengthText = std::getenv("APS5_EXP_LENGTH");
const std::uint32_t length = lengthText != nullptr && *lengthText != '\0' ? static_cast<std::uint32_t>(std::stoul(lengthText)) : 8192u;
const auto seed = static_cast<std::uint32_t>(std::chrono::steady_clock::now().time_since_epoch().count());
for (std::uint32_t index = 0; index < 4u; ++index) BuildIlp(Programs[index], length, 0x3f800000u | ((seed + index * 7919u) & 0x7fffu) | 1u);
BuildIlp(Programs[4], 16u, 0x3f800000u | ((seed * 3u) & 0x7fffu) | 1u);
const char* mode = std::getenv("APS5_LOCKED_PIPELINE") != nullptr ? "locked" : "unlocked";
std::vector<std::uint32_t> warm;
Dispatch(warm, Programs[4]);
WriteData(warm, Flags[0], 1);
const auto warmStart = std::chrono::steady_clock::now();
Submit(0x28, warm);
WaitFor(Flags[0], warmStart);
std::vector<std::uint32_t> first;
Dispatch(first, Programs[0]);
WriteData(first, Flags[1], 1);
std::vector<std::uint32_t> second;
Dispatch(second, Programs[1]);
WriteData(second, Flags[2], 1);
const auto pairStart = std::chrono::steady_clock::now();
Submit(0x20, first);
Submit(0x28, second);
const double firstMs = WaitFor(Flags[1], pairStart);
const double secondMs = WaitFor(Flags[2], pairStart);
std::vector<std::uint32_t> big;
Dispatch(big, Programs[2]);
WriteData(big, Flags[3], 1);
std::deque<std::vector<std::uint32_t>> probes;
std::vector<double> waits;
double worstAt = 0;
const auto bigStart = std::chrono::steady_clock::now();
Submit(0x20, big);
while (Flags[3].value == 0) {
Require(std::chrono::steady_clock::now() - bigStart < std::chrono::seconds(180), "the big submission did not finish");
Flags[4].value = 0;
auto& small = probes.emplace_back();
Dispatch(small, Programs[4]);
WriteData(small, Flags[4], 1);
const auto smallStart = std::chrono::steady_clock::now();
Submit(0x28, small);
waits.push_back(WaitFor(Flags[4], smallStart));
if (waits.back() >= *std::max_element(waits.begin(), waits.end())) worstAt = Since(bigStart) - waits.back();
std::this_thread::sleep_for(std::chrono::milliseconds(200));
}
const double bigMs = Since(bigStart);
auto sorted = waits;
std::sort(sorted.begin(), sorted.end());
std::fprintf(stderr, "[queues] %s, %u ops: two new programs on two queues done after %.0f and %.0f ms; %zu cached dispatches on another queue while one compiled (done after about %.0f ms): median wait %.0f ms, worst %.0f ms (submitted at %.0f ms)\n",
mode, length, firstMs, secondMs, waits.size(), bigMs, sorted[sorted.size() / 2u], sorted.back(), worstAt);
AgcDriverWaitIdle_nid_postfix();
AgcDriverShutdown_nid_postfix();
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
try {
AgcDriverShutdown_nid_postfix();
} catch (const std::exception& shutdownError) {
std::cerr << shutdownError.what() << '\n';
}
return 1;
}
} |
|
@Celegans12, your Ghost of Yōtei set from #4339 is the case this PR is about. On
|
|
Three things before this can go in:
|
CnRJay
left a comment
There was a problem hiding this comment.
I read through this and it looks correct. It is the right shape for moving a compile out of GpuMutex.
- The pipeline is built from the same
ShaderResourceslayout the dispatch binds (owned by the dispatch, or kept alive byPreparedDispatch::cached). It can't diverge from whatdispatch()would have built, so no extra compiles. - In-flight dedup through
computePipelinesBuildingmeans two queues that miss on the same variant compile it once. On failure both get the exception through the promise, and the entry is erased under the same mutex. PrepareDispatchhas one caller (Driver::dispatch), so no pipeline is built for a dispatch that never runs.- Teardown runs while device replacement holds the lock exclusively, and execution holds
deviceReplacementshared for the dispatch, socomputePipelinesBuilding.clear()can't race a build still in flight. - The measurement is the right one for this change: the worst wait of another queue during a compile (7.8 s → 3–4 ms). The limits section is honest that the dispatching queue still waits.
Small points:
- If
finishitself throws (try_emplaceallocation),buildingis destroyed without a value and waiters getbroken_promiseinstead of the original error. That is unlikely, butset_exceptionbefore the map update would avoid it. - The rule for whether to create here (
built.Layout() != VK_NULL_HANDLE && (pushStages == 0 || maxPushConstantsSize >= ComputePushConstantBytes)) is duplicated from the checks indispatch(). A shared helper would keep the two from drifting apart. - A title summary line (for example #2292 or Demon's Souls, where the stalls were reported) would complete the
perf(evidence that CONTRIBUTING asks for. The synthetic test already shows the mechanism.
|
I compared this moved compute-pipeline path with #4795. That PR adds If #4795 lands first, this pre-dispatch path would bypass its guard; if this lands first, rebasing #4795 will move the call site it currently patches. Please thread the context's pipeline-cache synchronization through |
|
Update: #4795 has now merged, and current |
…d to current main Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks @karanvora2599. Carried in #5989: your commit rebased onto current |
What
A compute pipeline is now created in
PrepareDispatch, before the dispatch takesGuestMemory::GpuMutex, instead of inVulkanDevice::dispatchunder it.Background.
main, the first dispatch of a compiled variant creates its pipeline inVulkanDevice::dispatch, whichDriver::dispatchcalls underGpuMutex. While the driver compiles, everything else that takesGpuMutexwaits: other queues' dispatches and draws,Driver::Present, label writes and memory waits. Two queues that each need a new pipeline compile one after the other.VK_PIPELINE_CREATE_DISABLE_OPTIMIZATION_BITchanges neither compile time nor run time for compute on that driver, so a quick unoptimized pipeline first doesn't help there.How.
VulkanDevice::computePipeline: module, layout, specialization, subgroup size, feat(agc): capture and replay compute pipeline compilation #3717's capture and the statistics.computePipelinereturns the pipeline fromcomputePipelines, or creates and inserts it.shared_futureper key incomputePipelinesBuilding. So two queues that need the same new variant create it once.PrepareDispatchcalls it once the descriptor set layout is known: from stage A, or from the cachedShaderResources. The result travels inPreparedDispatch::objects, anddispatch()uses it.dispatch()still creates the pipeline under the lock, through the same function, in three cases:Complete());APS5_LOCKED_BUILD=1;APS5_LOCKED_PIPELINE=1, which keepsmain's behaviour for comparisons.APS5_PROFILE_DRAW=1, a pipeline over 100K words now also prints[dispatch] created a pipeline for N SPIR-V words in M ms (program 0x...).Why creating it without the lock is safe.
ShaderResourcesthat the dispatch owns, or thatPreparedDispatch::cachedkeeps alive;workgroupMemoryLayout, made at device creation;ShaderResourcessets its layout in its build and clears it only inrelease(), which runs from the destructor or a failed build. A cached object has finished its build.VK_PIPELINE_CACHE_CREATE_EXTERNALLY_SYNCHRONIZED_BIT, so creating pipelines with it from several threads is valid (pipeline cache). Graphics library prebuilds (PrebuildStageLibrary) and the library job thread inPipelineLibrary.cppalready create pipelines with it outside the lock.main, the map entry made by the first dispatch of a variant already serves its later dispatches the same way.Before / after on an RTX 4060 Laptop (Ada, driver 591.86).
sceAgcDriverSubmitAcb.APS5_SPIRV_OPT=none.APS5_LOCKED_PIPELINE=1(main's behaviour) and this change.APS5_LOCKED_PIPELINE=1Limits.
PrepareDispatch. It has since been narrowed to the indirect draw checks and leaves that lookup to this PR.CreateComputePipelinecall takes that lock incomputePipeline.Tested
Windows 11, MinGW-w64 GCC 15.2.0, Release. Devices:
GALLIUM_OVERRIDE_CPU_CAPS=avx.The full runs were on
mainat 43e3a70. The branch is now rebased onto d1fd937, where I reran the tests under Dispatch path on the three devices, plusagc_driver_graphics, and repeated the measurement once each way (21.1 s → 14.1 s; worst wait 7.8 s → 4 ms). They pass.PrepareDispatchand the pipeline map. They pass on the three devices, withAPS5_VERIFY_RECIPE=1on the RTX 4060, and withAPS5_LOCKED_PIPELINE=1(the old path) on the RTX 4060:agc_driver_prepared_*,agc_driver_compute_user_data_calls*,agc_driver_gated_dispatch_capture*;agc_driver_oversized_*,agc_driver_flip_compute,agc_driver_wave32_subgroup,agc_driver_resource_cache_oversized_storage_admission;agc_compute_pipeline_capture,agc_compute_pipeline_replayandagc_compute_pipeline_capture_run(feat(agc): capture and replay compute pipeline compilation #3717's capture, which moved intocomputePipeline).agc_driver_*,agc_shader_*,agc_spirv_*,vector_*,agc_masked_select,agc_validate_program,agc_denormal*,agc_float16_*,agc_graphics; 352 tests):agc_driver_flat_loads*,agc_driver_global_loads*,agc_driver_flat_apertures*,agc_driver_pixel_interlock.agc_driver_ds_swizzle, which fails the same way onmain.agc_driver_depth_fast_clear_submit. It segfaults within 0.4 s in about half its runs, at the same address invulkan_lvp.dll(+0x10fe588) asmain's build of the test.Checklist
main; no other open PR implements the same functionsNotImplemented_nid_no_patch); silent stubs are listed in TechnicalDebt.mdfiles or images added to the repository (attach them to this PR)