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
getNumDevicesof theDeviceManagerclass can be accessed in thePoplarsubmodule 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
getNumDevicesof theDeviceManagerclass doesn't take any argument, so the Julia functionPoplar.DeviceManagerGetNumDeviceswill take an instance ofDeviceManageras 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.TensorAllocatedIn 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.TensorAllocatedAdds 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() -> NothingDetach 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.DeviceAllocatedSimilar 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 tondevices, starting from the one with indexhint. The default ishint=0; - if of type
AbstractVector, try to attach tondevices 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.DeviceAllocatedAttach 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] exprThis 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