Поделиться через


Рабочая отправка в режиме пользователя

Внимание

Некоторые сведения относятся к предварительному продукту, который может быть существенно изменен до его коммерческого выпуска. Майкрософт не предоставляет никаких гарантий, явных или подразумеваемых, относительно приведенных здесь сведений.

В этой статье описывается функция отправки в режиме пользователя (UM), которая по-прежнему находится в процессе разработки с Windows 11 версии 24H2 (WDDM 3.2). Рабочая отправка единой системы обмена сообщениями позволяет приложениям отправлять работу на GPU непосредственно из пользовательского режима с очень низкой задержкой. Цель заключается в повышении производительности приложений, которые часто передают небольшие рабочие нагрузки в GPU. Кроме того, ожидается, что отправка в режиме пользователя значительно повышает преимущества таких приложений, если они выполняются в контейнере или виртуальной машине. Это преимущество связано с тем, что драйвер пользовательского режима (UMD), работающий на виртуальной машине, может напрямую отправлять работу в GPU, не отправляя сообщение на узел.

Драйверы и оборудование IHV, поддерживающие рабочую отправку единой системы обмена сообщениями, должны продолжать поддерживать традиционную модель отправки в режиме ядра одновременно. Эта поддержка необходима для таких сценариев, как старый гость, который поддерживает только традиционные очереди KM, работающие на последнем узле.

В этой статье не рассматривается взаимодействие отправки единой системы обмена сообщениями с Flip/FlipEx. Отправка единой системы обмена сообщениями, описанная в этой статье, ограничена отображением только вычислительных классов сценариев. Конвейер презентации по-прежнему основан на отправке в режиме ядра, так как он имеет зависимость от собственных отслеживаемых заборов. Разработка и реализация презентации на основе отправки единой системы обмена сообщениями можно рассматривать после полной реализации собственных отслеживаемых заборов и отправки единой системы обмена сообщениями только для вычислений и отрисовки. Таким образом, драйверы должны поддерживать отправку в режиме пользователя на основе очереди.

Дверные звонки

Большинство текущих или предстоящих поколений gpu, поддерживающих планирование оборудования, также поддерживают концепцию двери GPU. Doorbell — это механизм, указывающий подсистеме GPU, что новая работа находится в очереди в своей рабочей очереди. Doorbells обычно регистрируется в PCIe BAR (базовой адресной строке) или системной памяти. Каждый GPU IHV имеет собственную архитектуру, которая определяет количество дверей, где они находятся в системе, и т. д. Ос Windows использует двери в рамках своей разработки для реализации рабочей отправки единой системы обмена сообщениями.

На высоком уровне существуют две разные модели дверей, реализованные различными IHV и GPU:

  • Глобальные двери

    В модели Global Doorbells все очереди оборудования в контекстах и процессах совместно используют одну глобальную дверь. Значение, записанное в doorbell, сообщает планировщику GPU о том, какая конкретная очередь оборудования и ядро имеют новую работу. Оборудование GPU использует форму механизма опроса для получения работы, если несколько аппаратных очередей активно отправляют работу и звонят одинаковой глобальной двери.

  • Выделенные двери

    В выделенной модели doorbell каждая очередь оборудования назначается собственной двери, которая выполняется всякий раз, когда в GPU будет отправлена новая работа. Когда выполняется дверь, планировщик GPU точно знает, какая очередь оборудования отправила новую работу. Существуют ограниченные двери, которые совместно используются во всех аппаратных очередях, созданных на GPU. Если количество созданных очередей оборудования превышает количество доступных дверей, водитель должен отключить дверь более старой или наименее недавно использованной очереди оборудования и назначить ее двери в только что созданную очередь, эффективно "виртуализацию" двери.

Обнаружение поддержки отправки в режиме пользователя

DXGK_NODEMETADATA_FLAGS::UserModeSubmissionSupported

Для узлов GPU, поддерживающих функцию отправки рабочей системы обмена сообщениями, параметр KMD dxgkDdiGetNodeMetadata задает флаг метаданных узла UserModeSubmissionSupported узла, добавленный в DXGK_NODEMETADATA_FLAGS. Затем ОС позволяет UMD создавать отправку в режиме пользователя HWQueues и двери только на узлах, для которых установлен этот флаг.

DXGK_QUERYADAPTERINFOTYPE::D XGKQAITYPE_USERMODESUBMISSION_CAPS

Для запроса сведений, относящихся к двери, ОС вызывает функцию DXGkDdiQueryAdapterInfo с типом сведений о адаптере запросов DXGKQAITYPE_USERMODESUBMISSION_CAPS. KMD отвечает, заполняя структуру DXGK_USERMODESUBMISSION_CAPS своими сведениями о поддержке для отправки в режиме пользователя.

В настоящее время единственным ограничением является размер памяти doorbell (в байтах). Dxgkrnl нуждается в размер памяти doorbell по нескольким причинам:

  • Во время создания doorbell (D3DKMTCreateDoorbell), Dxgkrnl возвращает DoorbellCpuVirtualAddress в UMD. Прежде чем это сделать, Dxgkrnl сначала необходимо внутренне сопоставить с фиктивной страницей, потому что дверь еще не назначена и подключена. Размер двери необходим для выделения фиктивной страницы.
  • Во время подключения doorbell (D3DKMT Подключение Doorbell), Dxgkrnl необходимо повернуть DoorbellCpuVirtualAddress на DoorbellPhysicalAddress, предоставляемый KMD. Опять же, Dxgkrnl необходимо знать размер двери.

D3DDDI_CREATEHWQUEUEFLAGS::UserModeSubmission в D3DKMTCreateHwQueue

UMD задает добавленный флаг UserModeSubmission , добавленный в D3DDDI_CREATEHWQUEUEFLAGS для создания HWQueues, использующих модель отправки в режиме пользователя. HWQueues, созданные с помощью этого флага, не могут использовать обычный путь отправки в режиме ядра и должны полагаться на механизм отправки рабочих данных в очереди.

API-интерфейсы отправки в режиме пользователя

Следующие API пользовательского режима добавляются для поддержки отправки в режиме пользователя.

  • D3DKMTCreateDoorbell создает дверь для D3D HWQueue для отправки рабочих данных в режиме пользователя.

  • D3DKMT Подключение Doorbell подключает ранее созданную дверь к D3D HWQueue для отправки в режиме пользователя.

  • D3DKMTDeskdoorbell уничтожает ранее созданную дверь.

  • D3DKMTNotifyWorkSubmission уведомляет KMD о том, что новая работа была отправлена на HWQueue. Точка этой функции — это путь отправки с низкой задержкой, где KMD не участвует или не учитывается при отправке работы. Этот API полезен в сценариях, когда KMD необходимо получать уведомления при отправке работы на HWQueue. Драйверы должны использовать этот механизм в конкретных и редких сценариях, так как он включает в себя круговую поездку от UMD к KMD на каждую рабочую отправку, тем самым побеждая цель модели отправки в режиме пользователя с низкой задержкой.

Модель расположения памяти и выделения кольцевого буфера

  • UMD отвечает за создание кольцевого буфера и кольцевого элемента управления выделениями перед созданием двери.
  • UMD управляет временем существования кольцевого буфера и кольцевого элемента управления. Dxgkrnl не будет уничтожать эти выделения неявно, даже если соответствующий дверь будет уничтожен. UMD отвечает за выделение и уничтожение этих выделений. Тем не менее, чтобы предотвратить вредоносную программу режима пользователя, чтобы уничтожить эти выделения в то время как дверь в живых, Dxgkrnl принимает ссылку на них во время существования двери.
  • Единственный сценарий, в котором Dxgkrnl уничтожает выделение буфера кольца во время завершения устройства. Dxgkrnl уничтожает все HWQueues, doorbells и кольцевые выделения буферов, связанные с устройством.
  • До тех пор, пока выделение кольцевого буфера живо, ЦПВ кольцевого буфера всегда действителен и доступен для доступа к UMD независимо от состояния подключений к двери. То есть, место расположения буфера кольца не привязано к двери.
  • Когда KMD делает обратный вызов DXG, чтобы отключить дверь (т. е. вызывает DxgkCbDisconnectDoorbell с состоянием D3DDDI_DOORBELL_STATUS_DISCONNECTED_RETRY), Dxgkrnl поворачивает ЦПВ doorbell на фиктивную страницу. Он не вытесняет или не отображает выделение кольцевого буфера.
  • В случае любых сценариев потери устройства (TDR/GPU Stop/Page и т. д.), Dxgkrnl отключает дверь и помечает состояние как D3DDDI_DOORBELL_STATUS_DISCONNECTED_ABORT. Пользовательский режим отвечает за уничтожение HWQueue, doorbell, кольцевого буфера и повторного их создания. Это требование аналогично уничтожению и повторному созданию других ресурсов устройства в этом сценарии.

Приостановка контекста оборудования

При приостановке контекста оборудования dxgkrnl сохраняет активное подключение doorbell и кольцевой буфер (рабочий очереди). Таким образом, UMD может продолжать работу очереди в контексте; Эта работа просто не запланирована, пока контекст приостановлен. После возобновления и планирования контекста обработчик управления контекстом GPU (CMP) наблюдает новую указатель записи и рабочую отправку.

Эта логика аналогична текущей логике отправки в режиме ядра, где UMD может вызывать D3DKMTSubmitCommand с приостановленным контекстом. Dxgkrnl заквещает эту новую команду в HwQueue, но она просто не будет запланирована до последующего времени.

Следующая последовательность событий возникает во время приостановки и возобновления работы контекста оборудования.

  • Приостановка контекста оборудования:

    1. Dxgkrnl вызывает DxgkddiSuspendContext.
    2. KMD удаляет все HWQueues контекста из списка планировщика HW.
    3. Doorbells по-прежнему подключены, а выделение буфера или кольцевого буфера по-прежнему находится в резидентном режиме. UMD может записывать новые команды в HWQueue этого контекста, но GPU не обработает их, что аналогично сегодняшней отправке команд в режиме ядра в приостановленном контексте.
    4. Если KMD решит жертвовать дверью приостановленного HWQueue, то UMD теряет свое соединение. UMD может попытаться повторно подключить дверь и KMD назначит новую дверь в эту очередь. Намерение заключается в том, чтобы не остановить UMD, а вместо того, чтобы позволить ему продолжать отправлять работу, которую модуль HW может в конечном итоге обработать после возобновления контекста.
  • Возобновление аппаратного контекста:

    1. Dxgkrnl вызывает DxgkddiResumeContext.
    2. KMD добавляет все HWQueues контекста в список планировщика HW.

Переходы состояния ядра

В традиционной отправке в режиме ядра dxgkrnl отвечает за отправку новых команд в HWQueue и мониторинг прерываний от KMD. По этой причине Dxgkrnl имеет полное представление о том, когда подсистема активна и неактивна.

В рабочей отправке в режиме пользователя Dxgkrnl отслеживает, выполняется ли обработчик GPU с использованием времени ожидания TDR, поэтому если стоит инициировать переход к состоянию F1 раньше, чем в двух секунде времени ожидания TDR, KMD может запросить операционную систему для этого.

Для упрощения этого подхода были внесены следующие изменения:

  • Тип прерывания DXGK_INTERRUPT_GPU_ENGINE_STATE_CHANGE добавляется в DXGK_INTERRUPT_TYPE. KMD использует это прерывание для уведомления Dxgkrnl о переходе состояния двигателя, для которых требуется действие питания GPU или восстановление времени ожидания, например Active -> TransitionToF1 и Active -> Hung.

  • Структура данных прерывания EngineStateChange добавляется в DXGKARGCB_NOTIFY_INTERRUPT_DATA.

  • Перечисление DXGK_ENGINE_STATE добавляется для представления переходов состояния ядра для EngineStateChange.

Когда KMD вызывает прерывание DXGK_INTERRUPT_GPU_ENGINE_STATE_CHANGE с помощью EngineStateChange.NewState , установлено значение DXGK_ENGINE_STATE_TRANSITION_TO_F1, Dxgkrnl отключает все двери hWQueues на этом двигателе, а затем инициирует переход компонента питания F0 на F1.

Когда UMD пытается отправить новую работу в подсистему GPU в состоянии F1, необходимо повторно подключить дверь, что, в свою очередь, приводит к тому, что Dxgkrnl инициирует переход обратно в состояние питания F0.

Переходы уровня D-состояния подсистемы

Во время перехода состояния питания устройства D0 на D3 Dxgkrnl приостанавливает HWQueue, отключает дверь (вращая ЦПВ двери на фиктивную страницу) и обновляет состояние doorbellStatusCpuVirtualAddress до D3DDDI_DOORBELL_STATUS_DISCONNECTED_RETRY.

Если UMD вызывает D3DKMT Подключение Doorbell, когда GPU находится в D3, он заставляет Dxgkrnl проснуть GPU до D0. Dxgkrnl также отвечает за возобновление HWQueue и поворот двери ЦПVA в физическое расположение двери.

Происходит следующая последовательность событий.

  • Происходит выключение питания D0 до D3 GPU:

    1. Dxgkrnl вызывает DxgkddiSuspendContext для всех контекстов HW на GPU. KMD удаляет эти контексты из списка планировщика HW.
    2. Dxgkrnl отключает все двери.
    3. Dxgkrnl , возможно, вытесняет все выделения буфера кольца или кольцевого буфера из VRAM при необходимости. Это делается после приостановки и удаления всех контекстов из списка планировщика оборудования, чтобы оборудование не ссылалось на вытеснение памяти.
  • UMD записывает новую команду в HWQueue, когда GPU находится в состоянии D3:

    1. UMD видит дверь отключен, поэтому вызывает D3DKMT Подключение Doorbell.
    2. Dxgkrnl инициирует переход D0.
    3. Dxgkrnl делает все выделенные кольцевым буфером или кольцевым буфером выделения, если они были вытеснированы.
    4. Dxgkrnl вызывает функцию DXGkddiCreateDoorbell KMD, чтобы запросить, чтобы KMD сделали подключение к двери для этого HWQueue.
    5. Dxgkrnl вызывает DxgkddiResumeContext для всех HWContexts. KMD добавляет соответствующие очереди в список планировщика HW.

DDIs для отправки рабочих данных в режиме пользователя

DD-интерфейсы, реализованные KMD

Следующие DD в режиме ядра добавляются для KMD для реализации поддержки отправки в режиме пользователя.

  • DxgkDdiCreateDoorbell. Когда UMD вызывает D3DKMTCreateDoorbell для создания двери для HWQueue, Dxgkrnl делает соответствующий вызов этой функции, чтобы KMD мог инициализировать свои структуры двери.

  • DxgkDdi Подключение Doorbell. Когда UMD вызывает D3DKMT Подключение Doorbell, Dxgkrnl делает соответствующий вызов этой функции, чтобы KMD мог предоставить ЦПВ, сопоставленное с расположением физической двери, а также сделать необходимые подключения между объектом HWQueue, объектом doorbell, физическим адресом doorbell, планировщиком GPU и т. д.

  • DxgkDdiDisconnectDoorbell. Когда ОС хочет отключить определенную дверь, он вызывает KMD с этим DDI.

  • DxgkDdiDedoorbell. Когда UMD вызывает D3DKMTDedoorbell, Dxgkrnl делает соответствующий вызов этой функции, чтобы KMD мог уничтожить свои структуры двери.

  • DxgkDdiNotifyWorkSubmission. Когда UMD вызывает D3DKMTNotifyWorkSubmission, Dxgkrnl делает соответствующий вызов этой функции, чтобы KMD можно было получать уведомления о новых рабочих отправках.

Dxgkrnl-реализован DDI

Обратный вызов DxgkCbDisconnectDoorbell реализуется Dxgkrnl. KMD может вызвать эту функцию, чтобы уведомить Dxgkrnl , что KMD должен отключить определенную дверь.

Изменения забора хода выполнения очереди HW

Аппаратные очереди, работающие в рабочей модели отправки единой системы обмена сообщениями, по-прежнему имеют концепцию монотонного увеличения значения забора хода выполнения, которое UMD создает и записывает при завершении буфера команд. Чтобы dxgkrnl знал, имеет ли определенная очередь оборудования ожидающей работы, UMD необходимо обновить значение ограждения хода выполнения очереди непосредственно перед добавлением нового буфера команды в кольцевой буфер и сделать его видимым для GPU. CreateDoorbell.HwQueueProgressFenceLastQueuedValueCPUVirtualAddress — это сопоставление процесса чтения и записи в режиме пользователя последней очереди.

Важно убедиться, что значение очереди обновляется прямо перед тем, как новая отправка становится видимой для GPU. Ниже приведена рекомендуемая последовательность операций. Предполагается, что очередь HW неактивна, а последний готовый буфер имел значение забора хода выполнения N.

  • Создайте новое значение забора хода выполнения N+1.
  • Заполните буфер команд. Последняя инструкция буфера команд — это значение забора хода выполнения, записываемое в N+1.
  • Сообщите ОС нового значения очереди, установив значение *(HwQueueProgressFenceLastQueuedValueCPUVirtualAddress) равным N+1.
  • Сделайте буфер команды видимым для GPU, добавив его в кольцевой буфер.
  • Звонит дверью.

Нормальное и ненормальное завершение процесса

Следующая последовательность событий происходит во время нормального завершения процесса.

Для каждого HWQueue устройства или контекста:

  1. Dxgkrnl вызывает DxgkDdiDisconnectDoorbell , чтобы отключить дверь.
  2. Dxgkrnl ожидает завершения последней очереди HwQueueProgressFenceLastQueuedValueCPUVirtualAddress на GPU. Выделение кольцевого буфера или кольцевого буфера остается резидентным.
  3. Ожидание Dxgkrnl удовлетворено, и теперь он может уничтожить выделение буфера кольца или буфера кольца, а также объекты doorbell и HWQueue.

Следующая последовательность событий происходит во время ненормального завершения процесса.

  1. Dxgkrnl помечает устройство ошибкой.

  2. Для каждого контекста устройства Dxgkrnl вызывает DxgkddiSuspendContext для приостановки контекста. Выделение кольцевого буфера или кольцевого буфера по-прежнему является резидентным. KMD предопределит контекст и удаляет его из списка выполнения HW.

  3. Для каждого HWQueue контекста Dxglrnl:

    a. Вызывает DxgkDdiDisconnectDoorbell , чтобы отключить дверь.

    b. Уничтожает выделение буфера или буфера кольца, а также объекты doorbell и HWQueue.

Примеры псевдокода

Псевдокод рабочей отправки в UMD

Следующий псевдокод является базовым примером модели, которую UMD следует использовать для создания и отправки работы в HWQueues с помощью API-интерфейсов doorbell. Рассмотрим hHWqueue1 дескриптор HWQueue, созданный с флагом с UserModeSubmission помощью существующего API D3DKMTCreateHwQueue.

// Create a doorbell for the HWQueue
D3DKMT_CREATE_DOORBELL CreateDoorbell = {};
CreateDoorbell.hHwQueue = hHwQueue1;
CreateDoorbell.hRingBuffer = hRingBufferAlloc;
CreateDoorbell.hRingBufferControl = hRingBufferControlAlloc;
CreateDoorbell.Flags.Value = 0;

NTSTATUS ApiStatus =  D3DKMTCreateDoorbell(&CreateDoorbell);
if(!NT_SUCCESS(ApiStatus))
  goto cleanup;

assert(CreateDoorbell.DoorbellCPUVirtualAddress!=NULL && 
      CreateDoorbell.DoorbellStatusCPUVirtualAddress!=NULL);

// Get a CPUVA of Ring buffer control alloc to obtain write pointer.
// Assume the write pointer is at offset 0 in this alloc
D3DKMT_LOCK2 Lock = {};
Lock.hAllocation = hRingBufferControlAlloc;
ApiStatus = D3DKMTLock2(&Lock);
if(!NT_SUCCESS(ApiStatus))
  goto cleanup;

UINT64* WritePointerCPUVirtualAddress = (UINT64*)Lock.pData;

// Doorbell created successfully. Submit command to this HWQueue

UINT64 DoorbellStatus = 0;
do
{
  // first connect the doorbell and read status
  ApiStatus = D3DKMTConnectDoorbell(hHwQueue1);
  D3DDDI_DOORBELL_STATUS DoorbellStatus = *(UINT64*(CreateDoorbell.DoorbellStatusCPUVirtualAddress));

  if(!NT_SUCCESS(ApiStatus) ||  DoorbellStatus == D3DDDI_DOORBELL_STATUS_DISCONNECTED_ABORT)
  {
    // fatal error in connecting doorbell, destroy this HWQueue and re-create using traditional kernel mode submission.
    goto cleanup_fallback;
  }

  // update the last queue progress fence value
  *(CreateDoorbell.HwQueueProgressFenceLastQueuedValueCPUVirtualAddress) = new_command_buffer_progress_fence_value;

  // write command to ring buffer of this HWQueue
  *(WritePointerCPUVirtualAddress) = address_location_of_command_buffer;

  // Ring doorbell by writing the write pointer value into doorbell address. 
  *(CreateDoorbell.DoorbellCPUVirtualAddress) = *WritePointerCPUVirtualAddress;

  // Check if submission succeeded by reading doorbell status
  DoorbellStatus = *(UINT64*(CreateDoorbell.DoorbellStatusCPUVirtualAddress));
  if(DoorbellStatus == D3DDDI_DOORBELL_STATUS_CONNECTED_NOTIFY)
  {
      D3DKMTNotifyWorkSubmission(CreateDoorbell.hDoorbell);
  }

} while (DoorbellStatus == D3DDDI_DOORBELL_STATUS_DISCONNECTED_RETRY);

Жертва псевдокода doorbell в KMD

В следующем примере показано, как KMD может потребоваться "виртуализация" и совместно использовать доступные двери между HWQueues на gpu, использующих выделенные двери.

Псевдокод функции KMD VictimizeDoorbell() :

  • KMD решает, что логическая дверь hDoorbell1 подключена к PhysicalDoorbell1 необходимой жертве и отключению.
  • KMD вызывает DxgkrnlDxgkCbDisconnectDoorbellCB(hDoorbell1->hHwQueue).
    • Dxgkrnl поворачивает UMD-visible CPUVA этой двери на фиктивную страницу и обновляет значение состояния до D3DDDI_DOORBELL_STATUS_DISCONNECTED_RETRY.
  • KMD возвращает обратный контроль и выполняет фактическую жертвизацию или отключение.
    • KMD жертвует hDoorbell1 и отключает его.PhysicalDoorbell1
    • PhysicalDoorbell1 доступен для использования

Теперь рассмотрим следующий сценарий:

  1. Существует один физический дверь в PCI BAR с ЦПVA в режиме ядра, равным 0xfeedfeee. Объект doorbell, созданный для HWQueue, назначается это физическое значение двери.

    HWQueue KMD Handle: hHwQueue1
    Doorbell KMD Handle: hDoorbell1
    Doorbell CPU Virtual Address: CpuVirtualAddressDoorbell1 =>  0xfeedfeee // hDoorbell1 is mapped to 0xfeedfeee
    Doorbell Status CPU Virtual Address: StatusCpuVirtualAddressDoorbell1 => D3DDDI_DOORBELL_STATUS_CONNECTED
    
  2. Ос вызывает DxgkDdiCreateDoorbell другое HWQueue2:

    HWQueue KMD Handle: hHwQueue2
    Doorbell KMD Handle: hDoorbell2
    Doorbell CPU Virtual Address: CpuVirtualAddressDoorbell2 => 0 // this doorbell object isn't yet assigned to a physical doorbell  
    Doorbell Status CPU Virtual Address: StatusCpuVirtualAddressDoorbell2 => D3DDDI_DOORBELL_STATUS_DISCONNECTED_RETRY
    
    // In the create doorbell DDI, KMD doesn't need to assign a physical doorbell yet, 
    // so the 0xfeedfeee doorbell is still connected to hDoorbell1
    
  3. Вызовы ОС:DxgkDdiConnectDoorbellhDoorbell2

    // KMD needs to victimize hDoorbell1 and assign 0xfeedfeee to hDoorbell2. 
    VictimizeDoorbell(hDoorbell1);
    
    // Physical doorbell 0xfeedfeee is now free and can be used vfor hDoorbell2.
    // KMD makes required connections for hDoorbell2 with HW
    ConnectPhysicalDoorbell(hDoorbell2, 0xfeedfeee)
    
    return 0xfeedfeee
    
    // On return from this DDI, *Dxgkrnl* maps 0xfeedfeee to process address space CPUVA i.e:
    // CpuVirtualAddressDoorbell2 => 0xfeedfeee
    
    // *Dxgkrnl* updates hDoorbell2 status to connected i.e:
    // StatusCpuVirtualAddressDoorbell2 => D3DDDI_DOORBELL_STATUS_CONNECTED
    ``
    
    

Этот механизм не требуется, если GPU использует глобальные двери. Вместо этого в этом примере оба hDoorbell1 и hDoorbell2 будут назначены одинаковые 0xfeedfeee физические двери.