Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basic command buffer support to level zero adapter v2 #2532

Open
wants to merge 26 commits into
base: main
Choose a base branch
from

Conversation

Xewar313
Copy link
Contributor

@Xewar313 Xewar313 commented Jan 8, 2025

This pull request implements basic calls to command buffer in level zero v2 adapter. These calls are required by sycl graph functionality implemented inside llvm, such as record and replay.

@Xewar313 Xewar313 requested review from a team as code owners January 8, 2025 11:51
@Xewar313 Xewar313 requested a review from reble January 8, 2025 11:51
@github-actions github-actions bot added level-zero L0 adapter specific issues command-buffer Command Buffer feature addition/changes/specification labels Jan 8, 2025
@@ -14,6 +14,16 @@

#include <ur_api.h>

#include "../common.hpp"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those includes should not be needed here. Also, this file (and queue_api.cpp) is auto-generate so you can't modify this manually. You need to update https://github.com/oneapi-src/unified-runtime/blob/main/scripts/templates/queue_api.hpp.mako and run make generate

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, could you also add a comment to the queue_api.hpp.mako saying that queue_api.hpp is being auto-generated? We should have already added that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a small problem with removing all includes. As far as I understand, the queue_api.hpp.mako is being generated based on some file containing declarations (ur_api.hpp?), but all of these declarations only use structures that were defined inside UR (all starting with ur_*). However, I had to add function that uses ze_command_list_handle_t, because there is no respective class with ur_ prefix. And the problem is, that the ze_command_list_handle_t must be included, so I have to at least include "common.hpp" or "../common.hpp" - without that this simply won't work. Other option would be creating something like ur_command_list_handle_t, but I believe that should be a separate PR, because of scope of the change.

Copy link
Member

@igchor igchor Jan 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need to use ze_command_list_handle_t then it should be enough to just include ze_api.h, just add it (and the enqueueCommandBuffer declaration) to the https://github.com/oneapi-src/unified-runtime/blob/main/scripts/templates/queue_api.hpp.mako and call make generate

#include "queue_api.hpp"

struct command_buffer_profiling_t {
ur_exp_command_buffer_sync_point_t NumEvents;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we are naming variables/params using with all lower-case in v2

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case, I believe that command_list_cache should also be changed, right? (Its fields are named starting with upper-case)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, good point, we never got to fixing it

ur_context_handle_t Context;
// Device associated with this command buffer
ur_device_handle_t Device;
ze_command_list_handle_t ZeCommandList;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please use v2::raii::ze_command_list_handle_t from v2/common.hpp

* @param[out] CommandList The L0 command-list created by this function.
* @return UR_RESULT_SUCCESS or an error code on failure
*/
ur_result_t createMainCommandList(ur_context_handle_t Context,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to cache command lists like we do for non-command-buffer path?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know what the non-command-buffer path does, but for reference creating a pool of command-lists to use has been an idea we've had for the v1 adapter but never got around to. TODO comment that has since been removed

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


ze_command_list_handle_t ZeCommandList = nullptr;
UR_CALL(createMainCommandList(Context, Device, IsUpdatable, ZeCommandList));
try {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please move the try/catch block to cover the entire function and add try/catch to other functions as well, i.e. urCommandBufferCreateExp(...) try { ... } catch (...) { return exceptionToResult(std::current_exception()); } In v2, we have helpers functions that might throw so it's best to wrap every function with try/catch.

UR_CALL_THROWS(ur::level_zero::urDeviceRetain(Device));
}

void ur_exp_command_buffer_handle_t_::cleanupCommandBufferResources() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think urContextRelease/urDeviceRelease can actually fail.

I would suggest just removing this entire function and moving the logic to the destructor.

Copy link
Contributor

@EwanC EwanC Jan 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UR_CALL_THROWS can throw an exception, which is bad practice to do in C++ destructors. So if we want to do this move I think removing that thow behaviour the main thing.

Copy link
Contributor Author

@Xewar313 Xewar313 Jan 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about urKernelRelease? I also need to release it when releasing buffer, so I would also need to know if it can fail, and I am not sure if it is a case

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, technically, urKernelRelease can fail if zeKernelDestroy fails but if that happens and we throw from this function we can have a memory leak (if there are more unreleased kernels in the vector).

Also, is it really that useful to return an error from urCommandBufferReleaseExp?

Perhaps we could just log all the failures from urKernelRelease, urContextRelease, etc and always return UR_RESULT_SUCCESS in urCommandBufferReleaseExp This would ensure that all resources are (attempted to be) freed, even if there is some error during release of one of them.

ur_exp_command_buffer_command_handle_t_(ur_exp_command_buffer_handle_t,
uint64_t);

virtual ~ur_exp_command_buffer_command_handle_t_();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn;t need to be virtual I think

@Xewar313 Xewar313 requested a review from a team as a code owner January 10, 2025 14:19
@@ -19,6 +19,8 @@ from templates import helper as th
*
*/

// This file was generated basing on scripts/templates/queue_api.cpp.mako
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Do not edit. This file is auto generated from a template:
scripts/templates/queue_api.cpp.mako

} // namespace

std::pair<ze_event_handle_t *, uint32_t>
ur_exp_command_buffer_handle_t_::getWaitListView(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is identical to the implementation in queue. Please create simple WaitListView abstraction usable in both.

ze_command_list_handle_t &commandList) {

using queue_group_type = ur_device_handle_t_::queue_group_info_t::type;
// that should be call to queue getZeOrdinal,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This, together with the fact that we have no way to allocate events from the correct queue, makes me think we either need to defer creation of these objects until the first enqueue of the command buffer or urCommandBufferCreateExp should take a queue.
@EwanC thoughts?

I'm also thinking whether it wouldn't make sense just to make the CommandBuffer allocate a whole pool of events from the context. That way, when we need an event, we don't have to acquire context locks.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This, together with the fact that we have no way to allocate events from the correct queue, makes me think we either need to defer creation of these objects until the first enqueue of the command buffer or urCommandBufferCreateExp should take a queue.
@EwanC thoughts?

I'm not that keen on urCommandBufferCreateExp taking a queue. 1) it doesn't match the SYCL API, where we don't have a queue object when creating the UR command-buffer. 2) It is the opposite direction to where the OpenCL WG is going with command-buffers in KhronosGroup/OpenCL-Docs#1292 to separate the queues used on command-buffer creation and enqueue.

Are there only 2 types of queue ordinal relevant here, compute and copy? If so I would suggest creating command-lists for both. This PR doesn't have the v1 functionality of splitting commands from the UR command-buffers into compute and copy command-lists. But we saw this having good perf benefits on V1, so would be imagine creating a copy engine command-list is something we'll need to do at some point anyway.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there only 2 types of queue ordinal relevant here, compute and copy?

In v2 by default we are only going to use the compute ordinal, letting the UMD decide whether to offload the copy to a separate engine. So the answer here is: "just use compute". I was more thinking about the general direction.
What you say makes sense.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool, letting the UMD handle this consideration definitely sounds like it will simplify the adapter code

checkImmediateAppendSupport(context);

if (isUpdatable) {
UR_ASSERT(context->getPlatform()->ZeMutableCmdListExt.Supported,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't use these "asserts". Do a normal if and return UR_RESULT_ERROR_UNSUPPORTED_FEATURE.
I just dislike overloading the term "assert" to mean fail with error.

* @param[out] commandList The L0 command-list created by this function.
* @return UR_RESULT_SUCCESS or an error code on failure
*/
ur_result_t createMainCommandList(ur_context_handle_t context,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this isn't used anywhere.

std::ignore = kernelAlternatives;
std::ignore = command;
try {
UR_ASSERT(hKernel, UR_RESULT_ERROR_INVALID_NULL_HANDLE);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is near identical to the queue enqueue implementation. I imagine a lot of other similar enqueue methods will be likewise similar.

We need to come up with an abstraction that lets us share this implementation between queue and command buffers.

While reviewing this, I had a thought that a command buffer is a subset of the queue functionality with some extra bits. This sounds like this should be solvable with composition:

class cmds { // enqueuable? command_list?
  WaitViewList waitlist;
  CmdList cmdlist;
  event_pool events;
  enqueue_kernel enqueue_kernel(...);
  ...
}

class queue {
  cmds cmd;
}

class command_buffer {
  cmds cmd;
}

@igchor thoughts?

Copy link
Member

@igchor igchor Jan 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, also, we can simplify the enqueue operations by combining getSignalEvent and getWaitListView functions. I would move this functionality to the ur_command_list_handler_t (we might want to rename it) and implement it like this:

struct ur_command_list_handler_t {
  ur_command_list_handler_t(ur_context_handle_t hContext,
                            ur_device_handle_t hDevice,
                            const ur_queue_properties_t *pProps);

  ur_command_list_handler_t(ze_command_list_handle_t hZeCommandList,
                            bool ownZeHandle);

  std::tuple<ze_event_handle_t, uint32_t, ze_event_handle_t *>
  getSignalEventAndWaitList(ur_event_handle_t *hUserEvent,
                            ur_command_t commandType,
                            const ur_event_handle_t *phWaitEvents,
                            uint32_t numWaitEvents);

  raii::command_list_unique_handle commandList;
  std::vector<ze_event_handle_t> waitList;
  event_pool events;
};

then, in enqueue* we can do:

auto [signalEvent, numWaitEvents, waitEvent] = getSignalEventAndWaitList(...);
...
ZE2UR_CALL(zeCommandListAppenSomething(..., signalEvent, numWaitEvents, waitEvent));

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, right now, getSIgnalEvent returns ur_event_handle_t (not ze_event_handle_t) because in enqueueTimestampRecordingExp, we need to call timestamp-related functions on it. But actually, we already have a pointer to the ur_event - it's the same one that we pass as a first argument to the getSignalEvent.

It's better to make getSignalEventAndWaitList return ze_event_handle_t so that we can avoid checking for nullptr.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed getSignalEvent to use ze_event_handle_t instead of ur_event_handle_t, but I don't think that merging getSignalEvent with getWaitListView makes sense - due to some C++ weirdness, referencing variables bound using [] inside lambda is impossible. So following code is not compiling:

auto [signalEvent, numWaitEvents, waitEvent] = getSignalEventAndWaitList(...);
  hMem->unmapHostPtr(pMappedPtr, [&](void *src, void *dst, size_t size) {
    ZE2UR_CALL_THROWS(zeCommandListAppendMemoryCopy,
                      (commandListManager.getZeCommandList(), dst, src, size,
                       nullptr, numWaitEvents, waitEvent));
    memoryMigrated = true;
  });
}

This pattern is used across whole queue implementation, so I would have to call std::get on tuple inside function, what would be even less readable.

Copy link
Member

@igchor igchor Jan 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I forgot about this. In that case, instead of the tuple we can just use a custom structure:

struct events {
    ze_event_handle_t signalEvent;
    size_t numWaitEvents;
    ze_event_handle_t *waitEvents;
};

and then, the code just becomes:

auto e = getSignalEventAndWaitList(...);
  hMem->unmapHostPtr(pMappedPtr, [&](void *src, void *dst, size_t size) {
    ZE2UR_CALL_THROWS(zeCommandListAppendMemoryCopy,
                      (commandListManager.getZeCommandList(), dst, src, size,
                       nullptr, e.numWaitEvents, e.waitEvent));
    memoryMigrated = true;
  });
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another problem that I have noticed with that solution is inside enqueueTimestampRecordingExp - We call there only getWaitListView without getSignalEvent, because ze_event_handle_t is obtained from ur_event_handle_t method. So we will get signal event without needing it there (so the most optimal solution would be again splitting this merged method into two)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's skip this change for now. Returning 24 byte-sized structure might prevent register use.

Copy link
Member

@igchor igchor Jan 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Xewar313 Yeah, I don't think your current implementation is correct. You should still call getSignalEvent() as you need to somehow allocate the event (the passed handle is out handle only, it doesn't point to a valid event).

So, with getSignalEventAndWaitList you would do:

auto e = getSignalEventAndWaitList(phEvent, ...);

(*phEvent)->recordStartTimestamp();
assert(phEvent->getZeHandle() == e.signalEvent);
...

And that's the point, I don't think there is any use case where we need to only call getWaitList so it would be safer to have it combined.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pbalcer I don't think there should be any difference with current usage where we call those function one after another and only then use the resulting values.

If that's really something that you'd like to optimize we can just move the implementation to the header and make it inline.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Xewar313 you can leave the implementation as is for now (just fix enqueueTimestampRecordingExp) and we can implement getSignalEventAndWaitList in another PR.

std::pair<ze_event_handle_t *, uint32_t>
getWaitListView(const ur_event_handle_t *phWaitEvents,
uint32_t numWaitEvents);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

private:

uint64_t);

~ur_exp_command_buffer_command_handle_t_();

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

private:

@github-actions github-actions bot added the loader Loader related feature/bug label Jan 21, 2025
* See LICENSE.TXT
* SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
* Part of the Unified-Runtime Project, under the Apache License v2.0 with LLVM
* Exceptions. See LICENSE.TXT SPDX-License-Identifier: Apache-2.0 WITH
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

invalid rebase?

Copy link
Contributor Author

@Xewar313 Xewar313 Jan 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a strange one - for some reason code formatting formats this header, but it shouldn't be formatted

return UR_RESULT_SUCCESS;
}

ur_result_t ur_command_list_manager::closeCommandList() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be inside of command buffer. Immediate command lists never need to be closed.


auto zeSignalEvent = signalEvent ? signalEvent->getZeEvent() : nullptr;

ZE2UR_CALL(zeCommandListImmediateAppendCommandListsExp,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is command buffer specific.

Copy link
Contributor Author

@Xewar313 Xewar313 Jan 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually it is queue specific (since we need to enqueue command buffer, by appending command list to the queue execution list), however since it is modifying the command list, I feel like it makes more sense to add it here, instead of queue implementation. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, right. I'd move it to queue then. The "command list manager" is not meant to be a clean command list abstraction, but merely a place where we store and retrieve command list and associated state. Otherwise we risk pushing everything into this class.

} // namespace

std::pair<ze_event_handle_t *, uint32_t>
ur_exp_command_buffer_handle_t_::getWaitListView(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can be removed, right?

Copy link
Contributor

@pbalcer pbalcer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

directionally lgtm, but we can't do this refactoring piecemeal with cmdlist-related state duplicated between queue class and the new manager. That might lead to inconsistency issues/duplicating event pools etc.

context, device, std::move(zeCommandList), commandBufferDesc);
return UR_RESULT_SUCCESS;

} catch (const std::bad_alloc &) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would change this to catch ... - the cache can throw other things as well

urCommandBufferFinalizeExp(ur_exp_command_buffer_handle_t hCommandBuffer) try {
UR_ASSERT(hCommandBuffer, UR_RESULT_ERROR_INVALID_NULL_POINTER);
UR_ASSERT(!hCommandBuffer->isFinalized, UR_RESULT_ERROR_INVALID_OPERATION);
hCommandBuffer->closeCommandList();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UR_CALL?

ur_result_t
urCommandBufferFinalizeExp(ur_exp_command_buffer_handle_t hCommandBuffer) try {
UR_ASSERT(hCommandBuffer, UR_RESULT_ERROR_INVALID_NULL_POINTER);
UR_ASSERT(!hCommandBuffer->isFinalized, UR_RESULT_ERROR_INVALID_OPERATION);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not thread safe, I think you should move this (and setting isFinalized to closeCommandList).

: context(context), device(device),
eventPool(context->eventPoolCache.borrow(device->Id.value(), flags)),
zeCommandList(
std::forward<v2::raii::command_list_unique_handle>(commandList)),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

std::move()

reinterpret_cast<ze_command_list_handle_t>(hNativeHandle),
[ownZeQueue](ze_command_list_handle_t hZeCommandList) {
if (ownZeQueue) {
zeCommandListDestroy(hZeCommandList);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ZE_CALL_NOCHECK, this is needed to have proper logs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
command-buffer Command Buffer feature addition/changes/specification level-zero L0 adapter specific issues loader Loader related feature/bug
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants