Interfacing the Poplar SDK
A quick example of use of the Poplar SDK functionalities, available in the IPUToolkit.Poplar
submodule:
julia> using IPUToolkit.Poplar
julia> dm = Poplar.DeviceManager();
julia> Int(Poplar.DeviceManagerGetNumDevices(dm))
129
julia> device = Poplar.get_ipu_device();
[ Info: Trying to attach to device 0...
[ Info: Successfully attached to device 0
julia> Int(Poplar.DeviceGetId(device))
0
julia> Poplar.detach_devices()
A couple of basic examples of programs running on the IPU written using the interface to the Poplar SDK are available in the files examples/tutorial1.jl
and examples/tutorial2.jl
.
We automatically generate the bindings of the Poplar SDK using Clang.jl
and CxxWrap.jl
. There is not automatic documentation at the moment, but functions can be accessed from the Poplar
submodule. Also, the IPUToolkit.Poplar
submodule wraps a subset of the functionalities available in the Poplar SDK, do not expect to be able to use all functionalities. Remember that Julia does not use class-based object-oriented programming, class instances will usually be first arguments of the methods you want to use.
Function naming convention and signature is usually as follows:
- class name in CamelCase, followed by method name also in CamelCase. Note that first letter of method name is always uppercase in this naming convention, even if it is lowercase in the Poplar SDK. For example, the method
getNumDevices
of theDeviceManager
class can be accessed in thePoplar
submodule withPoplar.DeviceManagerGetNumDevices
; - the first argument of the function is the class instance. For example, to use the Julia function
Poplar.DeviceManagerGetNumDevices
, you need to pass as first argument an instance ofDeviceManager
; - the following arguments are the same as in the method you want to use in the SDK. For example, the method
getNumDevices
of theDeviceManager
class doesn't take any argument, so the Julia functionPoplar.DeviceManagerGetNumDevices
will take an instance ofDeviceManager
as only argument.
Convenient methods
In addition to this, for some functions (e.g. EngineWriteTensor
, EngineConnectStream
, EngineReadTensor
) we provide more user-friendly methods where the last argument can be a Julia's Array
, without having to pass additional arguments for pointers or array size. Furthermore, the custom functions Poplar.get_ipu_device
and Poplar.get_ipu_devices
can be used to access one more IPU devices, as shown in the example above.
Another function for which we provide a convenient method is Poplar.GraphAddConstant
:
Poplar.GraphAddConstant(graph, host_array)
adds the host_array
(a plain standard Julia Array
living on the host) to graph
, automatically inferring from host_array
the type and the shape of the tensor in the graph. This works also with host_array::Array{Float16}
.
You can slice a tensor with the usual Julia notation tensor[index1:index2]
, this corresponds to a call to Tensor.slice(index1, index2+1)
.
similar
can be used to add to graph
a tensor with the same shape and optionally element type as tensor
, while copyto!
can be used to copy elements of a CPU host array into an IPU tensor.
Using IPUToolkit.jl
without an IPU
While this package requires a physical IPU to use all the available features, you can still experiment with the IPU programming model even if you do not have access to a hardware IPU. The Poplar SDK provides a feature called IPU Model, which is a software emulation of the behaviour of the IPU hardware. While the IPU model comes with some limitations, it can be useful for testing or debugging.
To use the IPU model in IPUToolkit.jl
, define the device of your IPU program with Poplar.get_ipu_model
:
device = Poplar.get_ipu_model()
# Then the rest of the program continues as usual
target = Poplar.DeviceGetTarget(device)
graph = Poplar.Graph(target)
# ...
Base.copyto!
— Methodcopyto!(
graph::Poplar.Graph,
dest::Poplar.TensorAllocated,
src::Array
) -> Poplar.TensorAllocated
In the given graph
copies the elements of the CPU host array src
to the IPU tensor dest
, using Graph::setInitialValue
under the hood. The elements of src
must have a type corresponding to the type of dest
(e.g. Float16
for a half
IPU tensor, or Float32
for a float
IPU tensor). This function returns dest
.
Base.similar
— Methodsimilar(
graph::Poplar.Graph,
tensor::Union{Poplar.TensorAllocated,Array},
[type::DataType],
[debug::String]
) -> Poplar.TensorAllocated
Adds to graph
a variable tensor with the same shape as tensor
, which can be either an IPU tensor or a plain CPU host Array
, using Graph::addVariable
under the hood. If a type
(this is a Julia type, like Float32
or Int32
) argument is not passed, the same element type as tensor
will be automatically used. An optional debug
context can also be passed, as a String
. This function returns a pointer to the tensor added to the graph.
IPUToolkit.Poplar.detach_devices
— MethodPoplar.detach_devices() -> Nothing
Detach all devices previously attached in the current Julia session with Poplar.get_ipu_devices
, Poplar.get_ipu_device
, or Poplar.get_ipu_model
.
IPUToolkit.Poplar.get_ipu_device
— FunctionPoplar.get_ipu_device(hint::Union{AbstractVector{<:Integer},Integer}=0) -> Poplar.DeviceAllocated
Similar to Poplar.get_ipu_devices
, but request exactly one IPU device. If it can attach to a device, return that pointer only (not in a vector, like get_ipu_devices
), otherwise return nothing
.
See Poplar.get_ipu_model
for requesting an IPU Model.
You can release the device with Poplar.DeviceDetach(device)
. To release all devices previously attached with Poplar.get_ipu_device
, Poplar.get_ipu_devices
, or Poplar.get_ipu_model
use Poplar.detach_devices
.
The optional argument hint
suggests to which device IDs to try and attach. It can have different types:
- if of type
Integer
, try to attach to one device, starting from the one with indexhint
. The default ishint=0
; - if of type
AbstractVector
, try to attach to a device from that list of IDs.
IPUToolkit.Poplar.get_ipu_devices
— FunctionPoplar.get_ipu_devices(n::Int, hint::Union{AbstractVector{<:Integer},Integer}=0) -> Vector{Poplar.DeviceAllocated}
Try to attach to n
IPU devices, returns a vector of the pointers to the devices successfully attached to. You can release them with Poplar.DeviceDetach
(note that this function takes a single pointer as input, so you have to use broadcasting Poplar.DeviceDetach.(devices)
to release a vector of pointers).
The second optional argument hint
suggests to which device IDs to try and attach. It can have different types:
- if of type
Integer
, try to attach ton
devices, starting from the one with indexhint
. The default ishint=0
; - if of type
AbstractVector
, try to attach ton
devices from that list of IDs.
See Poplar.get_ipu_device
for requesting exactly one IPU device, and Poplar.get_ipu_model
for requesting an IPU Model. To release all devices previously attached with Poplar.get_ipu_devices
, Poplar.get_ipu_device
, or Poplar.get_ipu_model
use Poplar.detach_devices
.
IPUToolkit.Poplar.get_ipu_model
— FunctionPoplar.get_ipu_model(ipu_version::String="ipu2") -> Poplar.DeviceAllocated
Attach to an IPU Model, and return the attached device. This uses IPUModel::createDevice
under the hood.
The optional positional argument ipu_version::String
, ipu2
by default, represents the version of the IPU to emulate. Valid values for ipu_version
are ipu1
and ipu2
(for Mk1 and Mk2 IPU architectures respectively).
See Poplar.get_ipu_device
and Poplar.get_ipu_devices
for requesting one or mode hardware IPUs.
You can release the device with Poplar.DeviceDetach(device)
. To release all devices previously attached with Poplar.get_ipu_model
, Poplar.get_ipu_device
or Poplar.get_ipu_devices
use Poplar.detach_devices
.
IPUToolkit.Poplar.@graph
— Macro@graph [graph] expr
This is a convenient macro to automatically inject graph
as first argument of all function calls in the expression passed as last argument to the macro.
The graph
argument should be the graph object you want to pass as first argument to the function calls. If it is a local variable called exactly graph
, this argument can be omitted and this name will be used automatically.
This macro is not very sophisticated and will fail with complex expressions involving, for example, control flows like if
or for
. See the examples below.
Examples
julia> @macroexpand @graph begin
c1 = Poplar.GraphAddConstant(Float32[1.0, 1.5, 2.0, 2.5])
v1 = similar(c1, "v1")
copyto!(v1, Float32[4.0, 3.0, 2.0, 1.0])
Poplar.GraphSetTileMapping(c1, 0)
Poplar.GraphSetTileMapping(v1, 0)
end
quote
c1 = Poplar.GraphAddConstant(graph, Float32[1.0, 1.5, 2.0, 2.5])
v1 = similar(graph, c1, "v1")
copyto!(graph, v1, Float32[4.0, 3.0, 2.0, 1.0])
Poplar.GraphSetTileMapping(graph, c1, 0)
Poplar.GraphSetTileMapping(graph, v1, 0)
end