Remote Tensor API of GPU Plugin

The GPU plugin implementation of the ov::RemoteContext and ov::RemoteTensor interfaces supports GPU pipeline developers who need video memory sharing and interoperability with existing native APIs, such as OpenCL, Microsoft DirectX, or VAAPI. Using these interfaces allows you to avoid any memory copy overhead when plugging OpenVINO™ inference into an existing GPU pipeline. It also enables OpenCL kernels participating in the pipeline to become native buffer consumers or producers of the OpenVINO™ inference.

There are two interoperability scenarios supported by the Remote Tensor API:

  • The GPU plugin context and memory objects can be constructed from low-level device, display, or memory handles and used to create the OpenVINO™ ov::CompiledModel or ov::Tensor objects.

  • The OpenCL context or buffer handles can be obtained from existing GPU plugin objects, and used in OpenCL processing on the application side.

Class and function declarations for the API are defined in the following files:

  • Windows*: openvino/runtime/intel_gpu/ocl/ocl.hpp and openvino/runtime/intel_gpu/ocl/dx.hpp

  • Linux*: openvino/runtime/intel_gpu/ocl/ocl.hpp and openvino/runtime/intel_gpu/ocl/va.hpp

The most common way to enable the interaction of your application with the Remote Tensor API is to use user-side utility classes and functions that consume or produce native handles directly.

Context sharing between application and GPU plugin

GPU plugin classes that implement the ov::RemoteContext interface are responsible for context sharing. Obtaining a context object is the first step of sharing pipeline objects. The context object of the GPU plugin directly wraps OpenCL context, setting a scope for sharing ov::CompiledModel and ov::RemoteTensor objects. ov::RemoteContext object can be either created on top of an existing handle from native api or retrieved from the GPU plugin.

Once you obtain the context, you can use it to compile a new ov::CompiledModel or create ov::RemoteTensor objects. For network compilation, use a dedicated flavor of ov::Core::compile_model(), which accepts the context as an additional parameter.

Creation of RemoteContext from native handle

To create ov::RemoteContext object for user context, explicitly provide the context to the plugin using constructor for one of ov::RemoteContext derived classes.

cl_context ctx = get_cl_context();
ov::intel_gpu::ocl::ClContext gpu_context(core, ctx);
cl_command_queue queue = get_cl_queue();
ov::intel_gpu::ocl::ClContext gpu_context(core, queue);
VADisplay display = get_va_display();
ov::intel_gpu::ocl::VAContext gpu_context(core, display);
cl_context ctx = get_cl_context();
ov::intel_gpu::ocl::ClContext gpu_context(core, ctx);
cl_command_queue queue = get_cl_queue();
ov::intel_gpu::ocl::ClContext gpu_context(core, queue);
ID3D11Device\* device = get_d3d_device();
ov::intel_gpu::ocl::D3DContext gpu_context(core, device);

Getting RemoteContext from the plugin

If you do not provide any user context, the plugin uses its default internal context. The plugin attempts to use the same internal context object as long as plugin options are kept the same. Therefore, all ov::CompiledModel objects created during this time share the same context. Once the plugin options are changed, the internal context is replaced by the new one.

To request the current default context of the plugin use one of the following methods:

auto gpu_context = core.get_default_context("GPU").as<ov::intel_gpu::ocl::ClContext>();
// Extract ocl context handle from RemoteContext
cl_context context_handle = gpu_context.get();
auto gpu_context = compiled_model.get_context().as<ov::intel_gpu::ocl::ClContext>();
// Extract ocl context handle from RemoteContext
cl_context context_handle = gpu_context.get();

Memory sharing between application and GPU plugin

The classes that implement the ov::RemoteTensor interface are the wrappers for native API memory handles (which can be obtained from them at any time).

To create a shared tensor from a native memory handle, use dedicated create_tensor or create_tensor_nv12 methods of the ov::RemoteContext sub-classes. ov::intel_gpu::ocl::ClContext has multiple overloads of create_tensor methods which allow to wrap pre-allocated native handles with ov::RemoteTensor object or request plugin to allocate specific device memory. See code snippets below for more details.

void\* shared_buffer = allocate_usm_buffer(input_size);
auto remote_tensor = gpu_context.create_tensor(in_element_type, in_shape, shared_buffer);
cl_mem shared_buffer = allocate_cl_mem(input_size);
auto remote_tensor = gpu_context.create_tensor(in_element_type, in_shape, shared_buffer);
cl::Buffer shared_buffer = allocate_buffer(input_size);
auto remote_tensor = gpu_context.create_tensor(in_element_type, in_shape, shared_buffer);
cl::Image2D shared_buffer = allocate_image(input_size);
auto remote_tensor = gpu_context.create_tensor(in_element_type, in_shape, shared_buffer);
cl::Image2D y_plane_surface = allocate_image(y_plane_size);
cl::Image2D uv_plane_surface = allocate_image(uv_plane_size);
auto remote_tensor = gpu_context.create_tensor_nv12(y_plane_surface, uv_plane_surface);
auto y_tensor = remote_tensor.first;
auto uv_tensor = remote_tensor.second;
ov::intel_gpu::ocl::USMTensor remote_tensor = gpu_context.create_usm_host_tensor(in_element_type, in_shape);
// Extract raw usm pointer from remote tensor
void\* usm_ptr = remote_tensor.get();
auto remote_tensor = gpu_context.create_usm_device_tensor(in_element_type, in_shape);
// Extract raw usm pointer from remote tensor
void\* usm_ptr = remote_tensor.get();
ov::RemoteTensor remote_tensor = gpu_context.create_tensor(in_element_type, in_shape);
// Cast from base to derived class and extract ocl memory handle
auto buffer_tensor = remote_tensor.as<ov::intel_gpu::ocl::ClBufferTensor>();
cl_mem handle = buffer_tensor.get();

ov::intel_gpu::ocl::D3DContext and ov::intel_gpu::ocl::VAContext classes are derived from ov::intel_gpu::ocl::ClContext, thus they provide the functionality described above and extend it to allow creation of ov::RemoteTensor objects from ID3D11Buffer, ID3D11Texture2D pointers or the VASurfaceID handle respectively.

Direct NV12 video surface input

To support the direct consumption of a hardware video decoder output, the plugin accepts two-plane video surfaces as arguments for the create_tensor_nv12() function, which creates a pair or ov::RemoteTensor objects which represent the Y and UV planes.

To ensure that the plugin generates the correct execution graph for the NV12 dual-plane input, static preprocessing should be added before model compilation:

using namespace ov::preprocess;
auto p = PrePostProcessor(model);
p.input().tensor().set_element_type(ov::element::u8)
                  .set_color_format(ov::preprocess::ColorFormat::NV12_TWO_PLANES, {"y", "uv"})
                  .set_memory_type(ov::intel_gpu::memory_type::surface);
p.input().preprocess().convert_color(ov::preprocess::ColorFormat::BGR);
p.input().model().set_layout("NCHW");
auto model_with_preproc = p.build();

Since ov::intel_gpu::ocl::ClImage2DTensor (and derived classes) doesn’t support batched surfaces, if batching and surface sharing are required at the same time, inputs need to be set via the ov::InferRequest::set_tensors method with vector of shared surfaces for each plane:

ov::intel_gpu::ocl::ClImage2DTensor y_tensor = get_y_tensor();
ov::intel_gpu::ocl::ClImage2DTensor uv_tensor = get_uv_tensor();
infer_request.set_tensor("y", y_tensor);
infer_request.set_tensor("uv", uv_tensor);
infer_request.infer();
std::vector<ov::Tensor> y_tensors = {y_tensor_0, y_tensor_1};
std::vector<ov::Tensor> uv_tensors = {uv_tensor_0, uv_tensor_1};
infer_request.set_tensors("y", y_tensors);
infer_request.set_tensors("uv", uv_tensors);
infer_request.infer();

I420 color format can be processed in a similar way

Context & queue sharing

The GPU plugin supports creation of shared context from cl_command_queue handle. In that case opencl context handle is extracted from the given queue via OpenCL™ API, and the queue itself is used inside the plugin for further execution of inference primitives. Sharing the queue changes the behavior of the ov::InferRequest::start_async() method to guarantee that submission of inference primitives into the given queue is finished before returning control back to the calling thread.

This sharing mechanism allows to do pipeline synchronization on the app side and avoid blocking the host thread on waiting for the completion of inference. The pseudo-code may look as follows:

// ...

// initialize the core and read the model
ov::Core core;
auto model = core.read_model("model.xml");

// get opencl queue object
cl::CommandQueue queue = get_ocl_queue();
cl::Context cl_context = get_ocl_context();

// share the queue with GPU plugin and compile model
auto remote_context = ov::intel_gpu::ocl::ClContext(core, queue.get());
auto exec_net_shared = core.compile_model(model, remote_context);

auto input = model->get_parameters().at(0);
auto input_size = ov::shape_size(input->get_shape());
auto output = model->get_results().at(0);
auto output_size = ov::shape_size(output->get_shape());
cl_int err;

// create the OpenCL buffers within the context
cl::Buffer shared_in_buffer(cl_context, CL_MEM_READ_WRITE, input_size, NULL, &err);
cl::Buffer shared_out_buffer(cl_context, CL_MEM_READ_WRITE, output_size, NULL, &err);
// wrap in and out buffers into RemoteTensor and set them to infer request
auto shared_in_blob = remote_context.create_tensor(input->get_element_type(), input->get_shape(), shared_in_buffer);
auto shared_out_blob = remote_context.create_tensor(output->get_element_type(), output->get_shape(), shared_out_buffer);
auto infer_request = exec_net_shared.create_infer_request();
infer_request.set_tensor(input, shared_in_blob);
infer_request.set_tensor(output, shared_out_blob);

// ...
// execute user kernel
cl::Program program;
cl::Kernel kernel_preproc(program, "user_kernel_preproc");
kernel_preproc.setArg(0, shared_in_buffer);
queue.enqueueNDRangeKernel(kernel_preproc,
                           cl::NDRange(0),
                           cl::NDRange(input_size),
                           cl::NDRange(1),
                           nullptr,
                           nullptr);
// Blocking clFinish() call is not required, but this barrier is added to the queue to guarantee that user kernel is finished
// before any inference primitive is started
queue.enqueueBarrierWithWaitList(nullptr, nullptr);
// ...

// pass results to the inference
// since the remote context is created with queue sharing, start_async() guarantees that scheduling is finished
infer_request.start_async();

// execute some postprocessing kernel.
// infer_request.wait() is not called, synchonization between inference and post-processing is done via
// enqueueBarrierWithWaitList call.
cl::Kernel kernel_postproc(program, "user_kernel_postproc");
kernel_postproc.setArg(0, shared_out_buffer);
queue.enqueueBarrierWithWaitList(nullptr, nullptr);
queue.enqueueNDRangeKernel(kernel_postproc,
                           cl::NDRange(0),
                           cl::NDRange(output_size),
                           cl::NDRange(1),
                           nullptr,
                           nullptr);

// Wait for pipeline completion
queue.finish();

Limitations

  • Some primitives in the GPU plugin may block the host thread on waiting for the previous primitives before adding its kernels to the command queue. In such cases the ov::InferRequest::start_async() call takes much more time to return control to the calling thread as internally it waits for a partial or full network completion. Examples of operations: Loop, TensorIterator, DetectionOutput, NonMaxSuppression

  • Synchronization of pre/post processing jobs and inference pipeline inside a shared queue is user’s responsibility

  • Throughput mode is not available when queue sharing is used, i.e. only a single stream can be used for each compiled model.

Low-Level Methods for RemoteContext and RemoteTensor creation

The high-level wrappers mentioned above bring a direct dependency on native APIs to the user program. If you want to avoid the dependency, you still can directly use the ov::Core::create_context(), ov::RemoteContext::create_tensor(), and ov::RemoteContext::get_params() methods. On this level, native handles are re-interpreted as void pointers and all arguments are passed using ov::AnyMap containers that are filled with std::string, ov::Any pairs. Two types of map entries are possible: descriptor and container. The first map entry is a descriptor, which sets the expected structure and possible parameter values of the map.

Refer to openvino/runtime/intel_gpu/remote_properties.hpp header file for possible low-level properties and their description.

Examples

Refer to the sections below to see pseudo-code of usage examples.

Note

For low-level parameter usage examples, see the source code of user-side wrappers from the include files mentioned above.

This example uses the OpenCL context obtained from a compiled model object.

// ...

// initialize the core and load the network
ov::Core core;
auto model = core.read_model("model.xml");
auto compiled_model = core.compile_model(model, "GPU");
auto infer_request = compiled_model.create_infer_request();


// obtain the RemoteContext from the compiled model object and cast it to ClContext
auto gpu_context = compiled_model.get_context().as<ov::intel_gpu::ocl::ClContext>();
// obtain the OpenCL context handle from the RemoteContext,
// get device info and create a queue
cl::Context cl_context = gpu_context;
cl::Device device = cl::Device(cl_context.getInfo<CL_CONTEXT_DEVICES>()[0].get(), true);
cl_command_queue_properties props = CL_QUEUE_OUT_OF_ORDER_EXEC_MODE_ENABLE;
cl::CommandQueue queue = cl::CommandQueue(cl_context, device, props);

// create the OpenCL buffer within the obtained context
auto input = model->get_parameters().at(0);
auto input_size = ov::shape_size(input->get_shape());
cl_int err;
cl::Buffer shared_buffer(cl_context, CL_MEM_READ_WRITE, input_size, NULL, &err);
// wrap the buffer into RemoteBlob
auto shared_blob = gpu_context.create_tensor(input->get_element_type(), input->get_shape(), shared_buffer);

// ...
// execute user kernel
cl::Program program;
cl::Kernel kernel(program, "user_kernel");
kernel.setArg(0, shared_buffer);
queue.enqueueNDRangeKernel(kernel,
                           cl::NDRange(0),
                           cl::NDRange(input_size),
                           cl::NDRange(1),
                           nullptr,
                           nullptr);
queue.finish();
// ...
// pass results to the inference
infer_request.set_tensor(input, shared_blob);
infer_request.infer();
cl::Context ctx = get_ocl_context();

ov::Core core;
auto model = core.read_model("model.xml");

// share the context with GPU plugin and compile ExecutableNetwork
auto remote_context = ov::intel_gpu::ocl::ClContext(core, ctx.get());
auto exec_net_shared = core.compile_model(model, remote_context);
auto inf_req_shared = exec_net_shared.create_infer_request();


// ...
// do OpenCL processing stuff
// ...

// run the inference
inf_req_shared.infer();
// ...

using namespace ov::preprocess;
auto p = PrePostProcessor(model);
p.input().tensor().set_element_type(ov::element::u8)
                  .set_color_format(ov::preprocess::ColorFormat::NV12_TWO_PLANES, {"y", "uv"})
                  .set_memory_type(ov::intel_gpu::memory_type::surface);
p.input().preprocess().convert_color(ov::preprocess::ColorFormat::BGR);
p.input().model().set_layout("NCHW");
model = p.build();

VADisplay disp = get_va_display();
// create the shared context object
auto shared_va_context = ov::intel_gpu::ocl::VAContext(core, disp);
// compile model within a shared context
auto compiled_model = core.compile_model(model, shared_va_context);

auto input = model->get_parameters().at(0);
size_t width = 1024;
size_t height = 768;

// execute decoding and obtain decoded surface handle
VASurfaceID va_surface = decode_va_surface();
//     ...
//wrap decoder output into RemoteBlobs and set it as inference input
auto nv12_blob = shared_va_context.create_tensor_nv12(height, width, va_surface);

auto infer_request = compiled_model.create_infer_request();
infer_request.set_tensor("y", nv12_blob.first);
infer_request.set_tensor("uv", nv12_blob.second);
infer_request.start_async();
infer_request.wait();