Skip to content
Snippets Groups Projects
Commit 1bd36647 authored by Olivier BICHLER's avatar Olivier BICHLER
Browse files

Merge branch 'view' into 'dev'

New proposal for handling tensor views

See merge request !69
parents 9f7c89aa deea0be0
No related branches found
No related tags found
2 merge requests!105version 0.2.0,!69New proposal for handling tensor views
Pipeline #37849 passed
# Version 0.1.0 (January 23, 2024)
Initial release
...@@ -67,19 +67,13 @@ private: ...@@ -67,19 +67,13 @@ private:
class TensorImpl { class TensorImpl {
public: public:
TensorImpl() = delete; TensorImpl() = delete;
TensorImpl(const char *backend, DeviceIdx_t device = 0) : mBackend(backend), mDevice(device){}; TensorImpl(const char *backend, DeviceIdx_t device, NbElts_t length) : mBackend(backend), mDevice(device), mNbElts(length) {};
/** /**
* Return the (backend, device) pair for this implementation. * Return the (backend, device) pair for this implementation.
*/ */
std::pair<std::string, DeviceIdx_t> device() const { return std::make_pair(mBackend, mDevice); } std::pair<std::string, DeviceIdx_t> device() const { return std::make_pair(mBackend, mDevice); }
/**
* Set the device ID for current backend.
* @param device New device ID on current backend.
*/
virtual void setDevice(DeviceIdx_t device) = 0;
/** /**
* Copy data from the same device. * Copy data from the same device.
* @param src Pointer on current implementation device. * @param src Pointer on current implementation device.
...@@ -93,30 +87,34 @@ public: ...@@ -93,30 +87,34 @@ public:
* @param srcDt Source data type. * @param srcDt Source data type.
* @param src Pointer on current implementation device. * @param src Pointer on current implementation device.
* @param length Number of elements to copy. * @param length Number of elements to copy.
* @param offset Destination offset (in number of elements).
*/ */
virtual void copyCast(const void *src, NbElts_t length, const DataType srcDt) = 0; virtual void copyCast(const void *src, const DataType srcDt, NbElts_t length, NbElts_t offset = 0) = 0;
/** /**
* Copy data from an other device on the same backend. * Copy data from an other device on the same backend.
* @param device (backend, device) pair to copy from. The backend must match current implementation backend. * @param device (backend, device) pair to copy from. The backend must match current implementation backend.
* @param src Pointer on current implementation backend. * @param src Pointer on current implementation backend.
* @param length Number of elements to copy. * @param length Number of elements to copy.
* @param offset Destination offset (in number of elements).
*/ */
virtual void copyFromDevice(const void *src, NbElts_t length, const std::pair<std::string, DeviceIdx_t>& device) = 0; virtual void copyFromDevice(const void *src, const std::pair<std::string, DeviceIdx_t>& device, NbElts_t length, NbElts_t offset = 0) = 0;
/** /**
* Copy data from host. * Copy data from host.
* @param src Host pointer to copy from. * @param src Host pointer to copy from.
* @param length Number of elements to copy. * @param length Number of elements to copy.
* @param offset Destination offset (in number of elements).
*/ */
virtual void copyFromHost(const void *src, NbElts_t length) = 0; virtual void copyFromHost(const void *src, NbElts_t length, NbElts_t offset = 0) = 0;
/** /**
* Copy data to host. * Copy data to host.
* @param src Host pointer to copy to. * @param src Host pointer to copy to.
* @param length Number of elements to copy. * @param length Number of elements to copy.
* @param offset Source offset (in number of elements).
*/ */
virtual void copyToHost(void *dst, NbElts_t length) const = 0; virtual void copyToHost(void *dst, NbElts_t length, NbElts_t offset = 0) const = 0;
/** /**
* Return the raw device pointer. * Return the raw device pointer.
...@@ -146,8 +144,22 @@ public: ...@@ -146,8 +144,22 @@ public:
AIDGE_THROW_OR_ABORT(std::runtime_error, "Cannot set raw pointer for backend %s", mBackend); AIDGE_THROW_OR_ABORT(std::runtime_error, "Cannot set raw pointer for backend %s", mBackend);
}; };
virtual std::size_t size() const = 0; // Storage size /**
virtual std::size_t scalarSize() const = 0; // Size of one scalar (in bytes) * Set the size, in number of elements, that must be stored.
*/
void resize(NbElts_t length) {
mNbElts = length;
}
/**
* Return the number of elements stored.
*/
inline std::size_t size() const noexcept { return mNbElts; }
/**
* Return the size (in bytes) of one element (scalar).
*/
virtual std::size_t scalarSize() const noexcept = 0;
constexpr const char *backend() const { return mBackend; } constexpr const char *backend() const { return mBackend; }
virtual ~TensorImpl() = default; virtual ~TensorImpl() = default;
virtual bool operator==(const TensorImpl &othImpl) const = 0; virtual bool operator==(const TensorImpl &othImpl) const = 0;
...@@ -156,12 +168,16 @@ public: ...@@ -156,12 +168,16 @@ public:
* Copy from another backend. * Copy from another backend.
* @param srcImpl Source TensorImpl to copy from. * @param srcImpl Source TensorImpl to copy from.
* @param length Number of elements of size scalarSize() to copy * @param length Number of elements of size scalarSize() to copy
* @param srcOffset Source offset (in number of elements).
* @param dstOffset Destination offset (in number of elements).
*/ */
void copyFrom(const TensorImpl& srcImpl, NbElts_t length); void copyFrom(const TensorImpl& srcImpl, NbElts_t length, NbElts_t srcOffset = 0, NbElts_t dstOffset = 0);
protected: protected:
const char *mBackend; const char *mBackend;
DeviceIdx_t mDevice; const DeviceIdx_t mDevice;
/// Number of elements (to be) stored
NbElts_t mNbElts;
}; };
} // namespace Aidge } // namespace Aidge
......
This diff is collapsed.
...@@ -30,7 +30,7 @@ void addCtor(py::class_<Tensor, ...@@ -30,7 +30,7 @@ void addCtor(py::class_<Tensor,
Data, Data,
Registrable<Tensor, Registrable<Tensor,
std::tuple<std::string, DataType>, std::tuple<std::string, DataType>,
std::unique_ptr<TensorImpl>(const Tensor&)>>& mTensor){ std::shared_ptr<TensorImpl>(DeviceIdx_t device, NbElts_t length)>>& mTensor){
mTensor.def(py::init([]( py::array_t<T, py::array::c_style | py::array::forcecast> b) { mTensor.def(py::init([]( py::array_t<T, py::array::c_style | py::array::forcecast> b) {
/* Request a buffer descriptor from Python */ /* Request a buffer descriptor from Python */
py::buffer_info info = b.request(); py::buffer_info info = b.request();
...@@ -58,16 +58,16 @@ void addCtor(py::class_<Tensor, ...@@ -58,16 +58,16 @@ void addCtor(py::class_<Tensor,
void init_Tensor(py::module& m){ void init_Tensor(py::module& m){
py::class_<Registrable<Tensor, py::class_<Registrable<Tensor,
std::tuple<std::string, DataType>, std::tuple<std::string, DataType>,
std::unique_ptr<TensorImpl>(const Tensor&)>, std::shared_ptr<TensorImpl>(DeviceIdx_t device, NbElts_t length)>,
std::shared_ptr<Registrable<Tensor, std::shared_ptr<Registrable<Tensor,
std::tuple<std::string, DataType>, std::tuple<std::string, DataType>,
std::unique_ptr<TensorImpl>(const Tensor&)>>>(m,"TensorRegistrable"); std::shared_ptr<TensorImpl>(DeviceIdx_t device, NbElts_t length)>>>(m,"TensorRegistrable");
py::class_<Tensor, std::shared_ptr<Tensor>, py::class_<Tensor, std::shared_ptr<Tensor>,
Data, Data,
Registrable<Tensor, Registrable<Tensor,
std::tuple<std::string, DataType>, std::tuple<std::string, DataType>,
std::unique_ptr<TensorImpl>(const Tensor&)>> pyClassTensor std::shared_ptr<TensorImpl>(DeviceIdx_t device, NbElts_t length)>> pyClassTensor
(m,"Tensor", py::multiple_inheritance(), py::buffer_protocol()); (m,"Tensor", py::multiple_inheritance(), py::buffer_protocol());
pyClassTensor.def(py::init<>()) pyClassTensor.def(py::init<>())
...@@ -76,7 +76,7 @@ void init_Tensor(py::module& m){ ...@@ -76,7 +76,7 @@ void init_Tensor(py::module& m){
.def("dims", (const std::vector<DimSize_t>& (Tensor::*)()const) &Tensor::dims) .def("dims", (const std::vector<DimSize_t>& (Tensor::*)()const) &Tensor::dims)
.def("dtype", &Tensor::dataType) .def("dtype", &Tensor::dataType)
.def("size", &Tensor::size) .def("size", &Tensor::size)
.def("resize", (void (Tensor::*)(const std::vector<DimSize_t>&)) &Tensor::resize) .def("resize", (void (Tensor::*)(const std::vector<DimSize_t>&, std::vector<DimSize_t>)) &Tensor::resize)
.def("has_impl", &Tensor::hasImpl) .def("has_impl", &Tensor::hasImpl)
.def("get_coord", &Tensor::getCoord) .def("get_coord", &Tensor::getCoord)
.def("get_idx", &Tensor::getIdx) .def("get_idx", &Tensor::getIdx)
...@@ -118,7 +118,7 @@ void init_Tensor(py::module& m){ ...@@ -118,7 +118,7 @@ void init_Tensor(py::module& m){
} }
}) })
.def_buffer([](Tensor& b) -> py::buffer_info { .def_buffer([](Tensor& b) -> py::buffer_info {
const std::unique_ptr<TensorImpl>& tensorImpl = b.getImpl(); const std::shared_ptr<TensorImpl>& tensorImpl = b.getImpl();
std::vector<size_t> dims; std::vector<size_t> dims;
std::vector<size_t> strides; std::vector<size_t> strides;
......
...@@ -14,23 +14,23 @@ ...@@ -14,23 +14,23 @@
#include "aidge/utils/Types.h" #include "aidge/utils/Types.h"
#include "aidge/utils/ErrorHandling.hpp" #include "aidge/utils/ErrorHandling.hpp"
void Aidge::TensorImpl::copyFrom(const TensorImpl& srcImpl, NbElts_t length) { void Aidge::TensorImpl::copyFrom(const TensorImpl& srcImpl, NbElts_t length, NbElts_t srcOffset, NbElts_t dstOffset) {
if (&srcImpl == this) { if (&srcImpl == this && srcOffset == dstOffset) {
return; return;
} }
if (srcImpl.device() != device()) { if (srcImpl.device() != device()) {
if (srcImpl.backend() == backend()) { if (srcImpl.backend() == backend()) {
// Same backend, but different device // Same backend, but different device
copyFromDevice(srcImpl.rawPtr(), length, srcImpl.device()); copyFromDevice(srcImpl.rawPtr(srcOffset), srcImpl.device(), length, dstOffset);
} }
else if (srcImpl.hostPtr() != nullptr) { else if (srcImpl.hostPtr() != nullptr) {
// Different backend, but input is valid on host // Different backend, but input is valid on host
copyFromHost(srcImpl.hostPtr(), length); copyFromHost(srcImpl.hostPtr(srcOffset), length, dstOffset);
} }
else if (hostPtr() != nullptr) { else if (hostPtr() != nullptr) {
// Different backend, but dst is valid on host // Different backend, but dst is valid on host
srcImpl.copyToHost(hostPtr(), length); srcImpl.copyToHost(hostPtr(srcOffset), length, dstOffset);
} }
else { else {
// No direct link possible from src to dst device // No direct link possible from src to dst device
...@@ -40,12 +40,12 @@ void Aidge::TensorImpl::copyFrom(const TensorImpl& srcImpl, NbElts_t length) { ...@@ -40,12 +40,12 @@ void Aidge::TensorImpl::copyFrom(const TensorImpl& srcImpl, NbElts_t length) {
// - There is currently no concrete use case // - There is currently no concrete use case
// - Just providing a pointer would be unsafe (risk of buffer overflow...) // - Just providing a pointer would be unsafe (risk of buffer overflow...)
auto tmpHostBuffer = std::unique_ptr<char[]>(new char[scalarSize() * length]); auto tmpHostBuffer = std::unique_ptr<char[]>(new char[scalarSize() * length]);
srcImpl.copyToHost(tmpHostBuffer.get(), length); srcImpl.copyToHost(tmpHostBuffer.get(), length, srcOffset);
copyFromHost(tmpHostBuffer.get(), length); copyFromHost(tmpHostBuffer.get(), length, dstOffset);
} }
} }
else { else {
// Same device: simple copy on device // Same device: simple copy on device
copy(srcImpl.rawPtr(), length); copy(srcImpl.rawPtr(srcOffset), length, dstOffset);
} }
} }
...@@ -13,11 +13,72 @@ ...@@ -13,11 +13,72 @@
#include "aidge/utils/Types.h" #include "aidge/utils/Types.h"
#include "aidge/utils/ErrorHandling.hpp" #include "aidge/utils/ErrorHandling.hpp"
Aidge::Tensor Aidge::Tensor::extract(const std::vector<std::size_t>& coordIdx) const {
AIDGE_ASSERT(isContiguous(), "Tensor must be contiguous");
AIDGE_ASSERT(coordIdx.size() <= mDims.size(), "Number of coordinates is higher than number of dimensions");
Tensor subTensor(mDataType);
subTensor.resize(std::vector<size_t>(mDims.begin() + coordIdx.size(), mDims.end()),
std::vector<size_t>(mStrides.begin() + coordIdx.size(), mStrides.end()));
subTensor.setBackend(mImpl->backend(), mImpl->device().second);
subTensor.setImpl(mImpl, mImplOffset + getStorageIdx(coordIdx));
return subTensor;
}
Aidge::Tensor Aidge::Tensor::extract(const std::vector<std::size_t>& coordIdx, const std::vector<std::size_t>& dims) const {
AIDGE_ASSERT(isContiguous(), "Tensor must be contiguous");
AIDGE_ASSERT(coordIdx.size() == mDims.size(), "Coordinates does not match number of dimensions");
Tensor subTensor(mDataType);
subTensor.resize(dims, mStrides);
subTensor.setBackend(mImpl->backend(), mImpl->device().second);
subTensor.setImpl(mImpl, mImplOffset + getStorageIdx(coordIdx));
return subTensor;
}
void Aidge::Tensor::makeContiguous() {
if (!mImpl || isContiguous()) {
return;
}
// Block so that mImpl ref count is 1 for resize()
{
// Create a new storage that will be contiguous
std::shared_ptr<TensorImpl> newImpl = Registrar<Tensor>::create({mImpl->backend(), mDataType})(mImpl->device().second, mSize);
// Copy elements from old to new storage
size_t idx = 0;
while (idx < mSize) {
const size_t storageIdx = getStorageIdx(getCoord(idx));
// Determine the size of the contiguous chunk
size_t copySize = 1;
while (idx + copySize < mSize &&
getStorageIdx(getCoord(idx + copySize)) == storageIdx + copySize)
{
++copySize;
}
// Perform a single copy for the contiguous chunk
newImpl->copy(mImpl->rawPtr(mImplOffset + storageIdx), copySize, idx);
// Move to the next index after the contiguous chunk
idx += copySize;
}
// Replace old storage by new, contiguous, storage
setImpl(newImpl);
}
// Resize tensor without strides => tensor is now contiguous
resize(mDims);
}
void Aidge::Tensor::copyCast(const Tensor& src) { void Aidge::Tensor::copyCast(const Tensor& src) {
if (&src == this) { if (&src == this) {
return; return;
} }
AIDGE_ASSERT(src.isContiguous(), "cannot copy-cast non-contiguous tensor");
// Current Tensor has necessarily a data type, but may not have backend // Current Tensor has necessarily a data type, but may not have backend
if (!getImpl()) { if (!getImpl()) {
// If no backend was set for the current tensor, use the same as src // If no backend was set for the current tensor, use the same as src
...@@ -27,7 +88,7 @@ void Aidge::Tensor::copyCast(const Tensor& src) { ...@@ -27,7 +88,7 @@ void Aidge::Tensor::copyCast(const Tensor& src) {
resize(src.dims()); resize(src.dims());
AIDGE_ASSERT(src.getImpl()->device() == getImpl()->device(), "cannot copy-cast from a different backend/device"); AIDGE_ASSERT(src.getImpl()->device() == getImpl()->device(), "cannot copy-cast from a different backend/device");
getImpl()->copyCast(src.getImpl()->rawPtr(), src.size(), src.dataType()); getImpl()->copyCast(src.getImpl()->rawPtr(src.mImplOffset), src.dataType(), src.size(), mImplOffset);
} }
void Aidge::Tensor::copyFrom(const Tensor& src) { void Aidge::Tensor::copyFrom(const Tensor& src) {
...@@ -35,6 +96,8 @@ void Aidge::Tensor::copyFrom(const Tensor& src) { ...@@ -35,6 +96,8 @@ void Aidge::Tensor::copyFrom(const Tensor& src) {
return; return;
} }
AIDGE_ASSERT(src.isContiguous(), "cannot copy from non-contiguous tensor");
// Current Tensor has necessarily a data type, but may not have backend // Current Tensor has necessarily a data type, but may not have backend
if (!getImpl()) { if (!getImpl()) {
// If no backend was set for the current tensor, use the same as src // If no backend was set for the current tensor, use the same as src
...@@ -44,7 +107,7 @@ void Aidge::Tensor::copyFrom(const Tensor& src) { ...@@ -44,7 +107,7 @@ void Aidge::Tensor::copyFrom(const Tensor& src) {
resize(src.dims()); resize(src.dims());
AIDGE_ASSERT(src.dataType() == dataType(), "cannot copy from a different data type"); AIDGE_ASSERT(src.dataType() == dataType(), "cannot copy from a different data type");
getImpl()->copyFrom(*(src.getImpl()), src.size()); getImpl()->copyFrom(*(src.getImpl()), src.size(), src.mImplOffset, mImplOffset);
} }
void Aidge::Tensor::copyCastFrom(const Tensor& src, std::shared_ptr<Tensor>& movedSrcPtr) { void Aidge::Tensor::copyCastFrom(const Tensor& src, std::shared_ptr<Tensor>& movedSrcPtr) {
...@@ -52,6 +115,8 @@ void Aidge::Tensor::copyCastFrom(const Tensor& src, std::shared_ptr<Tensor>& mov ...@@ -52,6 +115,8 @@ void Aidge::Tensor::copyCastFrom(const Tensor& src, std::shared_ptr<Tensor>& mov
return; return;
} }
AIDGE_ASSERT(src.isContiguous(), "cannot copy-cast from non-contiguous tensor");
// Current Tensor has necessarily a data type, but may not have backend // Current Tensor has necessarily a data type, but may not have backend
if (!getImpl()) { if (!getImpl()) {
// If no backend was set for the current tensor, use the same as src // If no backend was set for the current tensor, use the same as src
...@@ -65,12 +130,35 @@ void Aidge::Tensor::copyCastFrom(const Tensor& src, std::shared_ptr<Tensor>& mov ...@@ -65,12 +130,35 @@ void Aidge::Tensor::copyCastFrom(const Tensor& src, std::shared_ptr<Tensor>& mov
const auto device = getImpl()->device(); const auto device = getImpl()->device();
const Tensor& movedSrc = src.refFrom(movedSrcPtr, device.first, device.second); const Tensor& movedSrc = src.refFrom(movedSrcPtr, device.first, device.second);
// Second, copy-cast data (necessary) // Second, copy-cast data (necessary)
getImpl()->copyCast(movedSrc.getImpl()->rawPtr(), movedSrc.size(), movedSrc.dataType()); getImpl()->copyCast(movedSrc.getImpl()->rawPtr(movedSrc.mImplOffset), movedSrc.dataType(), movedSrc.size(), mImplOffset);
} }
else { else {
// Directly copy, no conversion necessary // Directly copy, no conversion necessary
// Avoid making a double copy if both data type and device are the same // Avoid making a double copy if both data type and device are the same
getImpl()->copyFrom(*(src.getImpl()), src.size()); getImpl()->copyFrom(*(src.getImpl()), src.size(), src.mImplOffset, mImplOffset);
}
}
Aidge::Tensor& Aidge::Tensor::refContiguous(std::shared_ptr<Tensor>& fallback) {
// Scott Meyers' solution to avoid code duplication
return const_cast<Tensor&>(static_cast<const Tensor&>(*this).refContiguous(fallback));
}
const Aidge::Tensor& Aidge::Tensor::refContiguous(std::shared_ptr<Tensor>& fallback) const {
AIDGE_ASSERT(getImpl(), "no backend was set for tensor, cannot refCast() it");
if (isContiguous()) {
return *this;
}
else {
if (this != fallback.get()) {
// Shallow copy to fallback
*fallback = *this;
}
// Make fallback contiguous
fallback->makeContiguous();
return *fallback;
} }
} }
...@@ -91,6 +179,8 @@ const Aidge::Tensor& Aidge::Tensor::refCast(std::shared_ptr<Tensor>& fallback, c ...@@ -91,6 +179,8 @@ const Aidge::Tensor& Aidge::Tensor::refCast(std::shared_ptr<Tensor>& fallback, c
fallback->setDataType(dt); fallback->setDataType(dt);
} }
else { else {
AIDGE_ASSERT(isContiguous(), "cannot refCast non-contiguous tensor");
if (!fallback) { if (!fallback) {
fallback = std::make_shared<Tensor>(dt); fallback = std::make_shared<Tensor>(dt);
} }
...@@ -101,7 +191,7 @@ const Aidge::Tensor& Aidge::Tensor::refCast(std::shared_ptr<Tensor>& fallback, c ...@@ -101,7 +191,7 @@ const Aidge::Tensor& Aidge::Tensor::refCast(std::shared_ptr<Tensor>& fallback, c
const auto device = getImpl()->device(); const auto device = getImpl()->device();
fallback->setBackend(device.first, device.second, false); // don't keep previous data (no copy) fallback->setBackend(device.first, device.second, false); // don't keep previous data (no copy)
fallback->resize(dims()); fallback->resize(dims());
fallback->getImpl()->copyCast(getImpl()->rawPtr(), size(), dataType()); fallback->getImpl()->copyCast(getImpl()->rawPtr(mImplOffset), dataType(), size(), fallback->mImplOffset);
} }
return *fallback; return *fallback;
} }
...@@ -124,6 +214,8 @@ const Aidge::Tensor& Aidge::Tensor::refFrom(std::shared_ptr<Tensor>& fallback, c ...@@ -124,6 +214,8 @@ const Aidge::Tensor& Aidge::Tensor::refFrom(std::shared_ptr<Tensor>& fallback, c
fallback->setBackend(backend, device); fallback->setBackend(backend, device);
} }
else { else {
AIDGE_ASSERT(isContiguous(), "cannot refFrom non-contiguous tensor");
if (!fallback) { if (!fallback) {
fallback = std::make_shared<Tensor>(dataType()); fallback = std::make_shared<Tensor>(dataType());
} }
...@@ -133,8 +225,34 @@ const Aidge::Tensor& Aidge::Tensor::refFrom(std::shared_ptr<Tensor>& fallback, c ...@@ -133,8 +225,34 @@ const Aidge::Tensor& Aidge::Tensor::refFrom(std::shared_ptr<Tensor>& fallback, c
fallback->setBackend(backend, device, false); // don't keep previous data (no copy) fallback->setBackend(backend, device, false); // don't keep previous data (no copy)
fallback->resize(dims()); fallback->resize(dims());
fallback->getImpl()->copyFrom(*getImpl(), size()); fallback->getImpl()->copyFrom(*getImpl(), size(), mImplOffset, fallback->mImplOffset);
}
return *fallback;
}
}
Aidge::Tensor& Aidge::Tensor::ref(std::shared_ptr<Tensor>& fallback, const Aidge::DataType& dt, const std::string &backend, DeviceIdx_t device) {
// Scott Meyers' solution to avoid code duplication
return const_cast<Tensor&>(static_cast<const Tensor&>(*this).ref(fallback, dt, backend, device));
}
const Aidge::Tensor& Aidge::Tensor::ref(std::shared_ptr<Tensor>& fallback, const Aidge::DataType& dt, const std::string &backend, DeviceIdx_t device) const {
AIDGE_ASSERT(getImpl(), "no backend was set for tensor, cannot ref() it");
if (dt == dataType() && std::make_pair(backend, device) == getImpl()->device()) {
return *this;
}
else {
// Change fallback type, backend & device, without any data copy
if (!fallback) {
fallback = std::make_shared<Tensor>(dt);
} }
else {
fallback->setDataType(dt, false); // don't keep previous data (no copy)
}
fallback->setBackend(backend, device, false); // don't keep previous data (no copy)
fallback->resize(dims());
return *fallback; return *fallback;
} }
} }
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment