Add MicroCUDA backend, adaptive output wait, wider native K coverage (improve perf)

Shim
- Adaptive output-event wait: sleep to ~120 us before the predicted
  completion (smoothed per wait site), spin near it, back off when late.
  D4R_SHIM_OUTPUT_POLL_US restores a fixed interval.

- Defer the input-marker wait into the worker thread with VRAM interop
  (D4R_SHIM_DEFER_MARKER, default on), taking the host setup off the
  critical path.

MicroCUDA
- Experimental ZLUDA-free backend in wine_nvcuda_bridge, plus
  tools/microcuda and scripts/build_microcuda.sh. Serves accuracy, fast
  and native kernels; unregistered modules fall back to the reference
  path. Unclassified writes invalidate prepared weights.

M kernels
- SWIN_PREP_IDENTITY_AT: key weight prep by allocation identity
  (MicroCUDA); enc3 tube uses it with 8 slots. ZLUDA ignores it.

- dec2 (gfx1101): SWIN_PACKED_RESIDUAL, byte-identical, ~1.6% faster.

- Opt-in gfx12 hardware fp8 conversion (SWIN_FP8_HWCVT), unverified
  on hardware.

Native K translators
- ctile: handle variants that re-materialize the parameter base (mvlo).

- rewrite_output: check live-in registers, not just outputs.

- build.sh: native input covers all 8 flag variants, output the 4 HDR
  "max" variants; depth-tile skip stays mvhi-only.

Tools
- dump_runner and bench_dlss_kernel harness additions.
This commit is contained in:
ayo committed 2026-10-07 15:54:38 -04:00
1 parent 7c60d53cd0
commit a627439025
20 files changed
+2186 -44

No files matched your search

+94 -7
View File
@@ -1615,6 +1615,7 @@ struct FrameTiming
double prep = 0, worker = 0, workerWait = 0, finishWait = 0, markerWait = 0;
double ngxHost = 0, ctxSync = 0, gpuEval = -1;
double motionDilationWall = 0;
bool markerDeferred = false; // the worker waits for the input marker (D4R_SHIM_DEFER_MARKER)
bool gpuEventsRecorded = false;
double outputSyncWall = 0, outputSyncCpu = -1;
bool outputSyncBlocking = false;
@@ -1680,6 +1681,9 @@ struct Feature
unsigned int preset = 0;
CudaEvent profileStart = nullptr, profileEnd = nullptr;
CudaEvent outputReadyEvent = nullptr;
// output wait pacing (D4R_SHIM_OUTPUT_POLL_US): smoothed time from the event record to its completion, kept
// separately for each wait site (they wait for very different amounts of GPU work)
double publishWaitEmaUs = 0.0, evalWaitEmaUs = 0.0;
bool profileEventsReady = false;
CudaImage color, depth, motion, exposure, output;
@@ -3757,16 +3761,59 @@ static bool upload_inputs_vram(Feature& feature, InputSlot& slot, bool hasExposu
// Only called on the CUDA worker. A completion event covers queued work on
// the default stream; do not rely on HIP's blocking flag to release the CPU.
static int synchronize_default_stream(Feature& feature, bool* querySleep = nullptr)
static int synchronize_default_stream(Feature& feature, bool* querySleep = nullptr, double* waitEmaUs = nullptr)
{
if (feature.outputReadyEvent != nullptr)
{
const int recorded = g.cu.eventRecord(feature.outputReadyEvent, nullptr);
if (recorded == 0)
{
// D4R_SHIM_OUTPUT_POLL_US: N > 0 polls every N us (200 was the fixed interval before), 0 yields between
// queries (spins). Unset: adaptive - sleep until shortly before the predicted completion (smoothed
// wait of earlier frames), then poll every 20 us, backing off to 200 us when the GPU runs late.
static const int fixedPoll = [] {
const char* v = std::getenv("D4R_SHIM_OUTPUT_POLL_US");
return v != nullptr && *v != '\0' ? static_cast<int>(env_uint("D4R_SHIM_OUTPUT_POLL_US", 200)) : -1;
}();
const auto waitStart = std::chrono::steady_clock::now();
unsigned polls = 0;
double lastNotReadyUs = -1.0; // elapsed time of the last query that found the work unfinished
const int result = d4r_wait_event(
[&] { return g.cu.eventQuery(feature.outputReadyEvent); },
[] { d4r_sleep_us(200); });
[&] {
if (fixedPoll >= 0)
{
d4r_sleep_us(static_cast<unsigned>(fixedPoll));
return;
}
constexpr double kMarginUs = 120.0, kStepUs = 20.0, kChunkUs = 200.0, kSpinAfterUs = 150.0;
const double elapsed = std::chrono::duration<double, std::micro>(
std::chrono::steady_clock::now() - waitStart).count();
lastNotReadyUs = elapsed; // this sleep follows a query that returned NOT_READY
++polls;
const double predicted = waitEmaUs != nullptr ? *waitEmaUs : 0.0;
const double remaining = predicted - kMarginUs - elapsed;
// far from the predicted completion: sleep in chunks of at most 200 us (a single long sleep
// can overshoot by far more under Wine); near it: 20 us polls; late: back off to 200 us
if (remaining > kStepUs)
d4r_sleep_us(static_cast<unsigned>(std::min(remaining, kChunkUs)));
else if (elapsed < predicted + kSpinAfterUs)
d4r_sleep_us(0); // final stretch around the predicted completion: spin (SwitchToThread);
// a Wine sleep rounds up to ~50 us, which would be the detection latency
else
d4r_sleep_us(static_cast<unsigned>(std::min(
kStepUs * (1.0 + (elapsed - predicted - kSpinAfterUs) / 160.0), kChunkUs)));
});
if (result == 0 && fixedPoll < 0 && waitEmaUs != nullptr)
{
const double waited = std::chrono::duration<double, std::micro>(
std::chrono::steady_clock::now() - waitStart).count();
// Learn the completion time, not our own wait: the work finished between the last NOT_READY query
// and the successful one (at most one 200 us chunk apart), so the midpoint cannot ratchet upwards
double sample = lastNotReadyUs >= 0.0 ? 0.5 * (lastNotReadyUs + waited) : waited;
sample = std::min(sample, 20000.0);
*waitEmaUs = *waitEmaUs == 0.0 ? sample : 0.8 * *waitEmaUs + 0.2 * sample;
}
if (result == 0)
{
if (querySleep != nullptr) *querySleep = true;
@@ -3811,7 +3858,7 @@ static void publish_vram(Feature* feature, uint32_t frame, const FrameParams& pa
{
const auto syncStart = timing.enabled ? ProfileClock::now() : ProfileClock::time_point{};
const double cpuStart = timing.enabled ? profile_thread_cpu_ms() : -1.0;
result = synchronize_default_stream(*feature, &timing.outputSyncBlocking);
result = synchronize_default_stream(*feature, &timing.outputSyncBlocking, &feature->publishWaitEmaUs);
if (timing.enabled)
{
timing.outputSyncWall = profile_ms(syncStart, ProfileClock::now());
@@ -3943,6 +3990,8 @@ static bool recreate_for_render_size(Feature& feature, FrameParams& params, uint
return feature.cudaHandle != nullptr && width == params.renderWidth && height == params.renderHeight;
}
static bool wait_for_input_marker(Feature* feature, int slotIndex, uint32_t frame);
static void run_evaluation(Feature* feature, int slotIndex, uint32_t frame, FrameParams params, FrameTiming timing)
{
InputSlot& slot = feature->inputs[slotIndex];
@@ -4039,6 +4088,14 @@ static void run_evaluation(Feature* feature, int slotIndex, uint32_t frame, Fram
}
feature->outputRedirected = redirect;
}
if (timing.markerDeferred)
{
const auto markerStart = ProfileClock::now();
if (!wait_for_input_marker(feature, slotIndex, frame))
return;
if (timing.enabled)
timing.markerWait = profile_ms(markerStart, ProfileClock::now());
}
const int eventStartResult = timing.enabled && feature->profileEventsReady
? g.cu.eventRecord(feature->profileStart, nullptr) : -1;
const auto evalStart = timing.enabled ? ProfileClock::now() : ProfileClock::time_point{};
@@ -4057,7 +4114,7 @@ static void run_evaluation(Feature* feature, int slotIndex, uint32_t frame, Fram
// D4R_SHIM_EVAL_SYNC=0 (VRAM interop): no wait between NGX's kernels and the output copy queued
// behind them on the same stream; publish_vram synchronises before the result is released.
static const bool evalSync = env_uint("D4R_SHIM_EVAL_SYNC", 1) != 0;
const int syncResult = params.vram && !evalSync ? 0 : synchronize_default_stream(*feature);
const int syncResult = params.vram && !evalSync ? 0 : synchronize_default_stream(*feature, nullptr, &feature->evalWaitEmaUs);
const auto evaluated = ProfileClock::now();
const bool gpuEventsRecorded = eventStartResult == 0 && eventEndResult == 0;
float gpuEvalMs = -1.0f;
@@ -4120,10 +4177,12 @@ static void run_evaluation(Feature* feature, int slotIndex, uint32_t frame, Fram
post_publish(feature, hostIndex, frame, params, timing);
}
static void prepare_inputs(Feature* feature, int slotIndex, uint32_t frame, FrameParams params, FrameTiming timing)
// Waits for this frame's own input marker (the GPU reached the game's input copies). false: the frame was
// dropped (retired feature, skipped submission, timeout) and has been retired here.
static bool wait_for_input_marker(Feature* feature, int slotIndex, uint32_t frame)
{
InputSlot& slot = feature->inputs[slotIndex];
const auto start = ProfileClock::now();
const auto start = std::chrono::steady_clock::now();
// A later input marker does not prove this slot's copies were submitted:
// the game may have discarded this frame's command list. Require its own
// tag, otherwise stale input data can enter DLSS's temporal history.
@@ -4143,7 +4202,7 @@ static void prepare_inputs(Feature* feature, int slotIndex, uint32_t frame, Fram
*inputMarker);
slot.busy = false;
frame_retired(feature, frame);
return;
return false;
}
// D4R_SHIM_MARKER_POLL_US: poll interval (0 = yield); detection latency is on the critical path
static const unsigned markerPollUs = env_uint("D4R_SHIM_MARKER_POLL_US", 200);
@@ -4152,6 +4211,34 @@ static void prepare_inputs(Feature* feature, int slotIndex, uint32_t frame, Fram
else
d4r_sleep_us(markerPollUs);
}
return true;
}
static void prepare_inputs(Feature* feature, int slotIndex, uint32_t frame, FrameParams params, FrameTiming timing)
{
InputSlot& slot = feature->inputs[slotIndex];
const auto start = ProfileClock::now();
// D4R_SHIM_DEFER_MARKER (default on): with VRAM interop nothing else happens here before the marker lands, so
// the worker thread waits for it itself, after the host-side setup of the evaluation (which no longer queues
// behind the marker: ~0.1 ms off the critical path) and without a thread wake-up between marker and NGX.
static const bool deferMarker = env_uint("D4R_SHIM_DEFER_MARKER", 1) != 0 &&
env_uint("D4R_MOTION_DILATION", 0) == 0;
if (params.vram && deferMarker && env_uint("D4R_SHIM_VRAM_VERIFY", 0) == 0)
{
timing.markerDeferred = true;
if (timing.enabled)
{
timing.prepStart = start;
timing.prep = profile_ms(start, ProfileClock::now());
timing.workerQueued = ProfileClock::now();
}
g.worker.post([feature, slotIndex, frame, params, timing] {
run_evaluation(feature, slotIndex, frame, params, timing);
});
return;
}
if (!wait_for_input_marker(feature, slotIndex, frame))
return;
const auto ready = ProfileClock::now();
// With VRAM interop the inputs are already on the GPU; host staging runs
// only to cross-check them (D4R_SHIM_VRAM_VERIFY).