Program Listing for File tensor.h

Program Listing for File tensor.h#

↰ Return to documentation for file (tvm/runtime/tensor.h)

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

#ifndef TVM_RUNTIME_TENSOR_H_
#define TVM_RUNTIME_TENSOR_H_

#include <tvm/ffi/cast.h>
#include <tvm/ffi/container/shape.h>
#include <tvm/ffi/container/tensor.h>
#include <tvm/ffi/dtype.h>
#include <tvm/ffi/optional.h>
#include <tvm/ffi/string.h>
#include <tvm/runtime/base.h>
#include <tvm/runtime/device_api.h>
#include <tvm/support/io.h>
#include <tvm/support/serializer.h>

#include <algorithm>
#include <atomic>
#include <functional>
#include <limits>
#include <mutex>
#include <unordered_map>
#include <utility>
#include <vector>

namespace tvm {
namespace runtime {

TVM_RUNTIME_DLL ffi::Tensor Empty(ffi::Shape shape, DLDataType dtype, Device dev,
                                  ffi::Optional<MemoryScope> mem_scope = std::nullopt);

TVM_RUNTIME_DLL void CopyFromTo(ffi::TensorView source, ffi::TensorView destination,
                                TVMStreamHandle stream = nullptr);

TVM_RUNTIME_DLL ffi::Tensor CopyTo(ffi::TensorView source, Device dev,
                                   ffi::Optional<MemoryScope> mem_scope = std::nullopt);

TVM_RUNTIME_DLL ffi::Tensor CreateView(ffi::Tensor source, ffi::Shape shape, DLDataType dtype,
                                       uint64_t relative_byte_offset = 0);

TVM_RUNTIME_DLL void CopyFromBytes(ffi::TensorView destination, const void* source_bytes,
                                   size_t nbytes, TVMStreamHandle stream = nullptr);

TVM_RUNTIME_DLL void CopyToBytes(ffi::TensorView source, void* destination_bytes, size_t nbytes,
                                 TVMStreamHandle stream = nullptr);

constexpr uint64_t kTVMTensorMagic = 0xDD5E40F096B4A13F;

TVM_RUNTIME_DLL void SaveTensor(ffi::TensorView source, support::Stream* strm);

TVM_RUNTIME_DLL ffi::Tensor LoadTensor(support::Stream* strm);

class TensorMemoryPool;

TVM_RUNTIME_DLL ffi::ObjectPtr<TensorMemoryPool> CreateTensorMemoryPool(DLDevice device);

class TensorMemoryPool : public ffi::Object {
 public:
  explicit TensorMemoryPool(size_t page_size = 4096) : page_size_(page_size), used_memory_(0) {
    TVM_FFI_CHECK_GT(page_size_, 0, ValueError) << "Pool page size must be positive";
    TVM_FFI_CHECK_LE(page_size_, static_cast<size_t>(std::numeric_limits<int64_t>::max()),
                     ValueError);
  }

  virtual ~TensorMemoryPool() { Clear(); }

  virtual ffi::Tensor Empty(ffi::ShapeView shape, DLDataType dtype, DLDevice device,
                            ffi::Optional<MemoryScope> mem_scope = std::nullopt) {
    std::lock_guard<std::recursive_mutex> lock(mu_);
    if (mem_scope.has_value() && *mem_scope != MemoryScope::Global()) {
      return DeviceAPI::Get(device)->Empty(shape, dtype, device, mem_scope);
    }
    size_t nbytes = ffi::GetDataSize(shape.Product(), dtype);
    TVM_FFI_CHECK_LE(std::max<size_t>(nbytes, 1),
                     static_cast<size_t>(std::numeric_limits<int64_t>::max()) - (page_size_ - 1),
                     ValueError)
        << "Rounded tensor allocation size overflows";
    size_t size = ((std::max<size_t>(nbytes, 1) + page_size_ - 1) / page_size_) * page_size_;
    size_t alignment = kAllocAlignment;
    size_t element_bytes = (static_cast<size_t>(dtype.bits) * dtype.lanes + 7) / 8;
    while (alignment < element_bytes) alignment *= 2;

    ffi::Tensor backing;
    auto it = memory_pool_.find(size);
    if (it != memory_pool_.end()) {
      auto& pool = it->second;
      bool addressable = DeviceAPI::Get(device)->SupportsDevicePtrArithmeticOnHost();
      auto candidate = std::find_if(pool.begin(), pool.end(), [&](const ffi::Tensor& tensor) {
        return tensor.device().device_type == device.device_type &&
               tensor.device().device_id == device.device_id &&
               (!addressable || reinterpret_cast<uintptr_t>(tensor.data_ptr()) % alignment == 0);
      });
      if (candidate != pool.end()) {
        backing = std::move(*candidate);
        pool.erase(candidate);
      }
    }
    if (!backing.defined()) {
      // Collective backends cannot retry by unilaterally releasing cached buffers.
      // Any allocation-pressure retry belongs to the non-collective adapter.
      backing = AllocBacking(size, alignment, device);
      used_memory_.fetch_add(size, std::memory_order_relaxed);
    }
    Lease lease{ffi::GetObjectPtr<TensorMemoryPool>(this), std::move(backing)};
    try {
      return ffi::Tensor::FromNDAlloc(lease, shape, dtype, device);
    } catch (...) {
      // Tensor metadata allocation may fail before the owning tensor exists.
      Recycle(std::move(lease.backing));
      throw;
    }
  }

  virtual ffi::Tensor CreateView(ffi::Tensor backing, ffi::Shape shape, DLDataType dtype,
                                 uint64_t byte_offset = 0,
                                 ffi::Optional<MemoryScope> mem_scope = std::nullopt) {
    auto view = runtime::CreateView(std::move(backing), std::move(shape), dtype, byte_offset);
    if (view.device().device_type == kDLHexagon &&
        (!mem_scope.has_value() || *mem_scope == MemoryScope::Global())) {
      // Hexagon kernels receive the adjusted pointer rather than a byte offset.
      auto* tensor = const_cast<DLTensor*>(view.GetDLTensorPtr());
      if (tensor->byte_offset != 0) {
        tensor->data = static_cast<char*>(tensor->data) + tensor->byte_offset;
      }
      tensor->byte_offset = 0;
    }
    return view;
  }

  void Clear() {
    std::lock_guard<std::recursive_mutex> lock(mu_);
    for (const auto& [size, pool] : memory_pool_) {
      used_memory_.fetch_sub(size * pool.size(), std::memory_order_relaxed);
    }
    memory_pool_.clear();
  }

  size_t UsedMemory() const { return used_memory_.load(std::memory_order_relaxed); }

  static constexpr bool _type_mutable = true;
  TVM_FFI_DECLARE_OBJECT_INFO("runtime.TensorMemoryPool", TensorMemoryPool, ffi::Object);

 protected:
  virtual ffi::Tensor AllocBacking(size_t nbytes, size_t alignment, DLDevice device) {
    auto allocate = [&]() {
      auto* api = DeviceAPI::Get(device);
      DLDataType bytes{kDLUInt, 8, 1};
      if (alignment <= kAllocAlignment) {
        return api->Empty({static_cast<int64_t>(nbytes)}, bytes, device);
      }
      // Preserve the backend's dtype alignment contract for wide vector elements.
      TVM_FFI_CHECK_LE(alignment, std::numeric_limits<uint16_t>::max(), ValueError);
      auto owner = api->Empty({static_cast<int64_t>((nbytes + alignment - 1) / alignment)},
                              DLDataType{kDLUInt, 8, static_cast<uint16_t>(alignment)}, device);
      return runtime::CreateView(std::move(owner), {static_cast<int64_t>(nbytes)}, bytes);
    };
    try {
      return allocate();
    } catch (const ffi::Error&) {
      // This default allocator is non-collective; release cached memory and retry.
      Clear();
      return allocate();
    }
  }

 private:
  struct Lease {
    ffi::ObjectPtr<TensorMemoryPool> pool;
    ffi::Tensor backing;

    void AllocData(DLTensor* tensor) { tensor->data = backing.data_ptr(); }
    void FreeData(DLTensor*) { pool->Recycle(std::move(backing)); }
  };

  void Recycle(ffi::Tensor backing) {
    std::lock_guard<std::recursive_mutex> lock(mu_);
    size_t size = ffi::GetDataSize(backing);
    try {
      memory_pool_[size].push_back(std::move(backing));
    } catch (const std::bad_alloc&) {
      // A cache insertion failure must not escape tensor destruction.
      used_memory_.fetch_sub(size, std::memory_order_relaxed);
    }
  }

  size_t page_size_;
  std::atomic<size_t> used_memory_;
  std::unordered_map<size_t, std::vector<ffi::Tensor>> memory_pool_;
  std::recursive_mutex mu_;
};

}  // namespace runtime
}  // namespace tvm

namespace std {
template <>
struct hash<tvm::Device> {
  std::size_t operator()(const tvm::Device& dev) const {
    return ((dev.device_id << 8) | dev.device_type);
  }
};

template <>
struct equal_to<tvm::Device> {
  bool operator()(const tvm::Device& lhs, const tvm::Device& rhs) const {
    return (lhs.device_type == rhs.device_type && lhs.device_id == rhs.device_id);
  }
};
}  // namespace std

#endif  // TVM_RUNTIME_TENSOR_H_