Current section
Files
Jump to
Current section
Files
c_src/exla/exla_client.cc
#include "exla_client.h"
#include "exla_nif_util.h"
#include "xla/layout_util.h"
#include "xla/pjrt/gpu/gpu_helpers.h"
#include "xla/pjrt/gpu/se_gpu_pjrt_client.h"
#include "xla/pjrt/pjrt_api.h"
#include "xla/pjrt/pjrt_c_api_client.h"
#include "xla/pjrt/pjrt_compiler.h"
#include "xla/pjrt/tfrt_cpu_pjrt_client.h"
#include "xla/shape_util.h"
namespace exla {
ExlaBuffer::ExlaBuffer(std::unique_ptr<xla::PjRtBuffer> buffer) : buffer_(std::move(buffer)) {}
void CopyLiteralToBinary(xla::Literal* literal, ErlNifBinary* binary, exla::int64 size) {
exla::int64 actual_size = literal->size_bytes();
if (size < 0 or size > actual_size) size = actual_size;
enif_alloc_binary(size, binary);
std::memcpy(binary->data, literal->untyped_data(), size);
}
xla::StatusOr<ERL_NIF_TERM> ExlaBuffer::ToBinary(ErlNifEnv* env, exla::int64 size) {
EXLA_ASSIGN_OR_RETURN(std::shared_ptr<xla::Literal> literal, buffer_->ToLiteralSync());
ErlNifBinary binary;
CopyLiteralToBinary(literal.get(), &binary, size);
return nif::make(env, binary);
}
xla::Status ExlaBuffer::Deallocate() {
if (buffer_->IsDeleted()) {
return xla::FailedPrecondition("Attempt to deallocate already deallocated buffer.");
} else {
buffer_->Delete();
return tsl::OkStatus();
}
}
xla::StatusOr<ExlaBuffer*> ExlaBuffer::CopyToDevice(xla::PjRtDevice* dst_device) {
EXLA_ASSIGN_OR_RETURN(std::unique_ptr<xla::PjRtBuffer> buf,
buffer_->CopyToDevice(dst_device));
return new ExlaBuffer(std::move(buf));
}
ExlaExecutable::ExlaExecutable(std::unique_ptr<xla::PjRtLoadedExecutable> executable,
absl::optional<std::string> fingerprint,
ExlaClient* client) : executable_(std::move(executable)),
fingerprint_(std::move(fingerprint)),
client_(client) {}
xla::StatusOr<std::unique_ptr<xla::PjRtBuffer>> PjRtBufferFromBinary(xla::PjRtClient* client,
ErlNifEnv* env,
ERL_NIF_TERM source_term,
const xla::Shape& shape,
int device_id) {
ErlNifEnv* copy_env = enif_alloc_env();
ERL_NIF_TERM dest_term = enif_make_copy(copy_env, source_term);
ErlNifBinary binary;
if (!nif::get_binary(copy_env, dest_term, &binary)) {
return xla::InvalidArgument("Expected buffer to be binary.");
}
xla::PjRtClient::HostBufferSemantics semantics = xla::PjRtClient::HostBufferSemantics::kImmutableZeroCopy;
std::function<void()> on_done_with_host_buffer = [copy_env]() { enif_free_env(copy_env); };
EXLA_ASSIGN_OR_RETURN(xla::PjRtDevice * device, client->LookupDevice(xla::PjRtGlobalDeviceId(device_id)));
// Passing std::nullopt should work, but it fails for subbyte types,
// so we build the default strides. See https://github.com/openxla/xla/issues/16795
auto byte_strides = xla::ShapeUtil::ByteStrides(shape);
EXLA_ASSIGN_OR_RETURN(auto buffer, client->BufferFromHostBuffer(
binary.data, shape.element_type(), shape.dimensions(), byte_strides, semantics, on_done_with_host_buffer, device));
return std::move(buffer);
}
xla::StatusOr<std::vector<xla::PjRtBuffer*>>
UnpackReplicaArguments(ErlNifEnv* env,
ERL_NIF_TERM replica_arguments,
ExlaClient* client,
int device) {
unsigned int length;
if (!enif_get_list_length(env, replica_arguments, &length)) {
return xla::InvalidArgument("Argument is not a list.");
}
ERL_NIF_TERM head, tail;
std::vector<xla::PjRtBuffer*> replica_buffers;
replica_buffers.reserve(length);
// for a single replica, the argument is a flat list of buffers where
// each buffer can either be an erlang binary or a reference to another
// EXLA buffer, it is not possible for any of the arguments to be nested
// tuples because we handle normalization/flattening of tuples on the
// Elixir side
while (enif_get_list_cell(env, replica_arguments, &head, &tail)) {
int arity;
const ERL_NIF_TERM* tuple;
ExlaBuffer** buffer;
if (enif_get_tuple(env, head, &arity, &tuple)) {
// if the term is a tuple, that means it represents a {shape, binary}
// tuple which we must convert into an exla buffer for use in the computation
xla::Shape shape;
if (!nif::get_typespec_as_xla_shape(env, tuple[1], &shape)) {
return xla::InvalidArgument("Expected argument to be a typespec.");
}
// we convert the binary into a buffer and transfer it to the correct device,
// this buffer is not managed by the erlang vm so it must be deallocated explicitly
// after use by the execution
EXLA_ASSIGN_OR_RETURN(std::unique_ptr<xla::PjRtBuffer> buf,
PjRtBufferFromBinary(client->client(), env, tuple[0], shape, device));
replica_buffers.push_back(buf.release());
} else if (nif::get<ExlaBuffer*>(env, head, buffer)) {
// if the buffer is not a tuple it must be a reference to an exla buffer
// which means the resource is already managed by the vm, and should already
// be on the correct device, if it is not, we will not do any implicit transfers
// and instead raise an error
if ((*buffer)->device_id() != device) {
return xla::InvalidArgument("Expected buffer to be placed on device %d", device);
}
replica_buffers.push_back((*buffer)->buffer());
} else {
return xla::InvalidArgument("Expected argument to be buffer reference.");
}
replica_arguments = tail;
}
return replica_buffers;
}
xla::StatusOr<std::vector<std::vector<xla::PjRtBuffer*>>>
UnpackRunArguments(ErlNifEnv* env,
ERL_NIF_TERM arguments,
ExlaClient* client,
xla::DeviceAssignment device_assignment,
int device_id) {
unsigned int length;
if (!enif_get_list_length(env, arguments, &length)) {
return xla::InvalidArgument("Argument is not a list.");
}
ERL_NIF_TERM head, tail;
std::vector<std::vector<xla::PjRtBuffer*>> arg_buffers;
arg_buffers.reserve(length);
int replica = 0;
int device;
while (enif_get_list_cell(env, arguments, &head, &tail)) {
device = device_id >= 0 ? device_id : device_assignment(replica, 0);
EXLA_ASSIGN_OR_RETURN(std::vector<xla::PjRtBuffer*> replica_buffers,
UnpackReplicaArguments(env, head, client, device));
arg_buffers.push_back(replica_buffers);
replica++;
arguments = tail;
}
return arg_buffers;
}
xla::StatusOr<ERL_NIF_TERM> UnpackResult(ErlNifEnv* env,
std::vector<std::vector<std::unique_ptr<xla::PjRtBuffer>>> result,
xla::DeviceAssignment device_assignment,
int device_id) {
std::vector<ERL_NIF_TERM> per_replica_results;
for (int i = 0; i < result.size(); i++) {
std::vector<ERL_NIF_TERM> terms;
terms.reserve(result.size());
int device = device_id >= 0 ? device_id : device_assignment(i, 0);
for (auto& pjrt_buf : result.at(i)) {
pjrt_buf->BlockHostUntilReady();
ExlaBuffer* buf = new ExlaBuffer(std::move(pjrt_buf));
ERL_NIF_TERM term = nif::make<ExlaBuffer*>(env, buf);
terms.push_back(term);
}
ERL_NIF_TERM replica_term = enif_make_int(env, device);
ERL_NIF_TERM replica_results = enif_make_list_from_array(env, terms.data(), terms.size());
per_replica_results.push_back(enif_make_tuple2(env, replica_results, replica_term));
}
ERL_NIF_TERM per_replica_term = enif_make_list_from_array(env, per_replica_results.data(), per_replica_results.size());
return nif::ok(env, per_replica_term);
}
void FreeReplicaArguments(ErlNifEnv* env, ERL_NIF_TERM replica_arguments, std::vector<xla::PjRtBuffer*> buffers) {
unsigned int length;
if (!enif_get_list_length(env, replica_arguments, &length)) {
return;
}
ERL_NIF_TERM head, tail;
int arg = 0;
while (enif_get_list_cell(env, replica_arguments, &head, &tail)) {
xla::PjRtBuffer* buffer = buffers.at(arg);
if (enif_is_tuple(env, head)) {
delete buffer;
}
arg++;
replica_arguments = tail;
}
}
void FreeRunArguments(ErlNifEnv* env, ERL_NIF_TERM arguments, std::vector<std::vector<xla::PjRtBuffer*>> buffers) {
unsigned int length;
if (!enif_get_list_length(env, arguments, &length)) {
return;
}
ERL_NIF_TERM head, tail;
int replica = 0;
while (enif_get_list_cell(env, arguments, &head, &tail)) {
FreeReplicaArguments(env, head, buffers.at(replica));
arguments = tail;
replica++;
}
}
xla::StatusOr<ERL_NIF_TERM> ExlaExecutable::Run(ErlNifEnv* env,
ERL_NIF_TERM arguments,
int device_id) {
xla::ExecuteOptions options;
// arguments are not passed as a single PjRt tuple buffer, but instead
// as multiple pjrt buffers
options.arguments_are_tupled = false;
// result is a tuple, which pjrt decomposes into a vector of buffers for
// us to handle ourselves
options.untuple_result = true;
// we do not handle multi-device launches at this time, so this must always
// be set to 0
options.launch_id = 0;
// disable strict shape checking which ensures shapes of buffers match exact
// shape (with layout) expected be compiled executable, we have mismatches
// on gpu
options.strict_shape_checking = false;
// execution mode determines whether or not to launch the executable in the
// calling thread or in a separate thread, default mode is either-or, here
// we specify synchronous because the Elixir side ensures execution is always
// synchronous
options.execution_mode = xla::ExecuteOptions::ExecutionMode::kSynchronous;
// the number of replicas will equal the number of devices involved in
// a pmap, but in all other cases it will be equal to 1
int num_replicas = executable_->num_replicas();
// input buffers are a list of lists, where each list maps to the args
// to pass to one of the replicas in a computation, e.g. [replica_args1, replica_args2, ...]
std::vector<std::vector<xla::PjRtBuffer*>> input_buffers;
// the device assignment is a 2d array which maps coordinates (replica, partition)
// to a device; or in this case just maps a replica to a device
xla::DeviceAssignment device_assignment;
if (client_->client()->platform_name() == "METAL") {
device_assignment = xla::DeviceAssignment(1, 1);
} else {
EXLA_ASSIGN_OR_RETURN(device_assignment,
client_->client()->GetDefaultDeviceAssignment(num_replicas, 1));
}
if (device_id >= 0 && num_replicas > 1) {
// if the device id is greater than or equal to 1, that means we've specified
// a portable executable which cannot be pmapped, this code path should never
// be reached as it should be controlled from Elixir
return xla::InvalidArgument("Cannot specify a device for replicated executable.");
} else {
// else we handle unpacking/validating the run arguments to the correct devices
// according to the device id and the device assignment
EXLA_ASSIGN_OR_RETURN(input_buffers, UnpackRunArguments(env, arguments, client_, device_assignment, device_id));
}
// at this point input buffers is a vector of arguments per replica
// and the size of that vector should equal the number of replicas in the
// executable, otherwise it is invalid
if (num_replicas != input_buffers.size()) {
return xla::InvalidArgument("Got %d replica arguments for %d replicas", input_buffers.size(), num_replicas);
}
std::vector<std::vector<std::unique_ptr<xla::PjRtBuffer>>> per_replica_results;
if (device_id >= 0) {
// if we specified a device id, then we need to execute the executable as a portable
// executable, meaning we need to find the device corresponding to the specific device
// id and execute on that device, we've already guaranteed this executable only has 1
// replica
EXLA_ASSIGN_OR_RETURN(xla::PjRtDevice * device, client_->client()->LookupDevice(xla::PjRtGlobalDeviceId(device_id)));
// because this is a portable executable, it only has 1 replica and so we only need
// to get the arguments at the first position of the input buffers
std::vector<xla::PjRtBuffer*> portable_args = input_buffers.at(0);
EXLA_ASSIGN_OR_RETURN(auto portable_result,
executable_->ExecutePortable(portable_args, device, options));
// the logic for handling unpacking of results is shared between portable code path
// and the replicated code-path, so we take ownership of the result buffers to unpack
per_replica_results.push_back(std::move(portable_result));
} else {
// no device ID is present, so it may be a replicated executable which means we need
// to use the replica execution path
// TODO: This now exposes a `returned_futures` API, does this make sense for us?
EXLA_ASSIGN_OR_RETURN(per_replica_results, executable_->Execute(input_buffers, options));
}
// EXLA_ASSIGN_OR_RETURN(per_replica_results, executable_->Execute(input_buffers, options));
// sanity check
if (per_replica_results.size() != num_replicas) {
return xla::FailedPrecondition("Invalid execution.");
}
// we need to unpack the results into Erlang terms, the result is a vector
// of vectors of unique ptrs to pjrt buffers, where the size of the output equals
// the number of replicas and each individual replica is a vector of buffers, the
// inner buffer represents a flattened output because we told PjRt we would always
// return a tuple from the computation
EXLA_ASSIGN_OR_RETURN(ERL_NIF_TERM ret,
UnpackResult(env, std::move(per_replica_results), device_assignment, device_id));
// finally, we need to free any of the arguments we created for this computation
FreeRunArguments(env, arguments, input_buffers);
return ret;
}
ExlaClient::ExlaClient(std::shared_ptr<xla::PjRtClient> client) : client_(std::move(client)) {}
xla::StatusOr<ExlaBuffer*> ExlaClient::BufferFromBinary(ErlNifEnv* env,
ERL_NIF_TERM source_term,
xla::Shape& shape,
int device_id) {
EXLA_ASSIGN_OR_RETURN(auto buffer, PjRtBufferFromBinary(client(), env, source_term, shape, device_id));
ExlaBuffer* exla_buffer = new ExlaBuffer(std::move(buffer));
return exla_buffer;
}
xla::StatusOr<std::optional<std::string>> ExecutableFingerprint(std::unique_ptr<xla::PjRtLoadedExecutable>& executable) {
auto fingerprint = executable->FingerprintExecutable();
if (fingerprint.ok()) {
return {fingerprint.value()};
} else if (fingerprint.status().code() == absl::StatusCode::kUnimplemented) {
// Return nullopt in case of unimplemented error.
return std::nullopt;
} else {
return fingerprint.status();
}
}
xla::StatusOr<ExlaExecutable*> ExlaClient::DeserializeExecutable(std::string deserialized_executable) {
EXLA_ASSIGN_OR_RETURN(std::unique_ptr<xla::PjRtLoadedExecutable> executable,
client_->DeserializeExecutable(deserialized_executable, std::nullopt));
EXLA_ASSIGN_OR_RETURN(absl::optional<std::string> fingerprint,
ExecutableFingerprint(executable));
return new ExlaExecutable(std::move(executable), std::move(fingerprint), this);
}
xla::StatusOr<ExlaExecutable*> ExlaClient::Compile(const mlir::OwningOpRef<mlir::ModuleOp>& module,
std::vector<xla::Shape> argument_layouts,
xla::ExecutableBuildOptions& options,
bool compile_portable_executable) {
std::vector<xla::Shape> layouts;
layouts.reserve(argument_layouts.size());
for (auto shape : argument_layouts) {
xla::Shape cpy_shape = xla::ShapeUtil::MakeShape(shape.element_type(), shape.dimensions());
xla::LayoutUtil::ClearLayout(&cpy_shape);
layouts.push_back(cpy_shape);
}
xla::CompileOptions compile_opts;
compile_opts.argument_layouts = layouts;
compile_opts.parameter_is_tupled_arguments = false;
compile_opts.executable_build_options = options;
compile_opts.compile_portable_executable = compile_portable_executable;
EXLA_ASSIGN_OR_RETURN(std::unique_ptr<xla::PjRtLoadedExecutable> executable,
client_->Compile(*module, std::move(compile_opts)));
EXLA_ASSIGN_OR_RETURN(absl::optional<std::string> fingerprint,
ExecutableFingerprint(executable));
return new ExlaExecutable(std::move(executable), std::move(fingerprint), this);
}
xla::Status ExlaClient::TransferToInfeed(ErlNifEnv* env,
std::vector<ErlNifBinary> buffer_bins,
std::vector<xla::Shape> shapes,
int device_id) {
std::vector<const char*> buf_ptrs;
buf_ptrs.reserve(buffer_bins.size());
for (const auto & buffer_bin : buffer_bins) {
const char* data_ptr = const_cast<char*>(reinterpret_cast<char*>(buffer_bin.data));
buf_ptrs.push_back(data_ptr);
}
auto shape = xla::ShapeUtil::MakeTupleShape(shapes);
// Instead of pushing each buffer separately, we create a flat tuple
// literal and push the whole group of buffers.
//
// On the CPU, XLA infeed reads buffers from a queue one at a time [1][2]
// (or rather, the infeed operation is lowered to multiple queue reads),
// hence pushing one at a time works fine. Pushing a flat tuple works
// effectively the same, since it basically adds each element to the
// queue [3].
//
// On the GPU, XLA infeed reads only a single "literal" from a queue [4]
// and expects it to carry all buffers for the given infeed operation.
// Consequently, we need to push all buffers as a single literal.
//
// Given that a flat tuple works in both cases, we just do that.
//
// [1]: https://github.com/openxla/xla/blob/fd58925adee147d38c25a085354e15427a12d00a/xla/service/cpu/ir_emitter.cc#L449-L450
// [2]: https://github.com/openxla/xla/blob/fd58925adee147d38c25a085354e15427a12d00a/xla/service/cpu/cpu_runtime.cc#L222
// [3]: https://github.com/openxla/xla/blob/fd58925adee147d38c25a085354e15427a12d00a/xla/service/cpu/cpu_xfeed.cc#L178
// [4]: https://github.com/openxla/xla/blob/fd58925adee147d38c25a085354e15427a12d00a/xla/service/gpu/runtime/infeed_thunk.cc#L40-L41
xla::BorrowingLiteral literal(buf_ptrs, shape);
EXLA_ASSIGN_OR_RETURN(xla::PjRtDevice * device, client_->LookupDevice(xla::PjRtGlobalDeviceId(device_id)));
xla::Status status = device->TransferToInfeed(literal);
return status;
}
xla::StatusOr<ERL_NIF_TERM> ExlaClient::TransferFromOutfeed(ErlNifEnv* env, int device_id, xla::Shape& shape) {
EXLA_ASSIGN_OR_RETURN(xla::PjRtDevice * device, client_->LookupDevice(xla::PjRtGlobalDeviceId(device_id)));
auto literal = std::make_shared<xla::Literal>(shape);
xla::Status transfer_status = device->TransferFromOutfeed(literal.get());
if (!transfer_status.ok()) {
return transfer_status;
}
ErlNifBinary binary;
enif_alloc_binary(literal->size_bytes(), &binary);
std::memcpy(binary.data, literal->untyped_data(), literal->size_bytes());
return nif::make(env, binary);
}
xla::StatusOr<ExlaClient*> GetHostClient() {
EXLA_ASSIGN_OR_RETURN(std::unique_ptr<xla::PjRtClient> client,
xla::GetTfrtCpuClient(false));
return new ExlaClient(std::move(client));
}
xla::StatusOr<ExlaClient*> GetGpuClient(double memory_fraction,
bool preallocate,
xla::GpuAllocatorConfig::Kind kind) {
xla::GpuAllocatorConfig allocator_config = {
.kind = kind,
.memory_fraction = memory_fraction,
.preallocate = preallocate};
xla::GpuClientOptions client_options = {
.allocator_config = allocator_config};
EXLA_ASSIGN_OR_RETURN(std::unique_ptr<xla::PjRtClient> client,
xla::GetStreamExecutorGpuClient(client_options));
return new ExlaClient(std::move(client));
}
xla::StatusOr<ExlaClient*> GetTpuClient() {
EXLA_EFFECT_OR_RETURN(pjrt::LoadPjrtPlugin("tpu", "libtpu.so"));
xla::Status status = pjrt::InitializePjrtPlugin("tpu");
if (!status.ok()) {
return status;
}
EXLA_ASSIGN_OR_RETURN(std::unique_ptr<xla::PjRtClient> client,
xla::GetCApiClient("TPU"));
return new ExlaClient(std::move(client));
}
xla::StatusOr<ExlaClient*> GetCApiClient(std::string device_type) {
EXLA_ASSIGN_OR_RETURN(std::unique_ptr<xla::PjRtClient> client,
xla::GetCApiClient(device_type));
return new ExlaClient(std::move(client));
}
} // namespace exla