import math
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch as th
def module_fn(
x: torch.Tensor,
clusters: torch.Tensor,
clusters2: torch.Tensor,
bn_weight: torch.Tensor,
bn_bias: torch.Tensor,
bn_running_mean: torch.Tensor,
bn_running_var: torch.Tensor,
feature_size: int,
cluster_size: int,
is_training: bool,
) -> torch.Tensor:
"""
Functional version of the NetVLAD without ghost clusters
Args:
x: Input tensor of shape (batch_size, num_features, feature_size)
clusters: Weight tensor for cluster assignments
clusters2: Weight tensor for visual words
bn_weight: BatchNorm weight
bn_bias: BatchNorm bias
bn_running_mean: BatchNorm running mean
bn_running_var: BatchNorm running var
feature_size: Size of each feature
cluster_size: Number of clusters (excluding ghost clusters)
is_training: Whether in training mode
Returns:
Output tensor of shape (batch_size, cluster_size * feature_size)
"""
max_sample = x.size()[1]
x = x.view(-1, feature_size) # B x N x D -> BN x D
if x.device != clusters.device:
msg = f"x.device {x.device} != cluster.device {clusters.device}"
raise ValueError(msg)
assignment = th.matmul(x, clusters) # (BN x D) x (D x (K+G)) -> BN x (K+G)
assignment = F.batch_norm(
assignment,
bn_running_mean,
bn_running_var,
bn_weight,
bn_bias,
training=is_training,
)
assignment = F.softmax(assignment, dim=1) # BN x (K+G) -> BN x (K+G)
# remove ghost assigments
assignment = assignment[:, :cluster_size]
assignment = assignment.view(-1, max_sample, cluster_size) # -> B x N x K
a_sum = th.sum(assignment, dim=1, keepdim=True) # B x N x K -> B x 1 x K
a = a_sum * clusters2
assignment = assignment.transpose(1, 2) # B x N x K -> B x K x N
x = x.view(-1, max_sample, feature_size) # BN x D -> B x N x D
vlad = th.matmul(assignment, x) # (B x K x N) x (B x N x D) -> B x K x D
vlad = vlad.transpose(1, 2) # -> B x D x K
vlad = vlad - a
# L2 intra norm
vlad = F.normalize(vlad)
# flattening + L2 norm
vlad = vlad.reshape(-1, cluster_size * feature_size) # -> B x DK
vlad = F.normalize(vlad)
return vlad # B x DK
class Model(nn.Module):
def __init__(self, cluster_size, feature_size, ghost_clusters):
super(Model, self).__init__()
self.feature_size = feature_size
self.cluster_size = cluster_size
self.ghost_clusters = ghost_clusters
init_sc = 1 / math.sqrt(feature_size)
clusters = cluster_size + ghost_clusters
# The `clusters` weights are the `(w,b)` in the paper
self.clusters = nn.Parameter(init_sc * th.randn(feature_size, clusters))
# Extract batchnorm parameters
bn = nn.BatchNorm1d(clusters)
self.bn_weight = nn.Parameter(bn.weight.data.clone())
self.bn_bias = nn.Parameter(bn.bias.data.clone())
self.bn_running_mean = nn.Parameter(bn.running_mean.data.clone())
self.bn_running_var = nn.Parameter(bn.running_var.data.clone())
# The `clusters2` weights are the visual words `c_k` in the paper
self.clusters2 = nn.Parameter(init_sc * th.randn(1, feature_size, cluster_size))
self.out_dim = self.cluster_size * feature_size
def forward(self, x, fn=module_fn):
return fn(
x,
self.clusters,
self.clusters2,
self.bn_weight,
self.bn_bias,
self.bn_running_mean,
self.bn_running_var,
self.feature_size,
self.cluster_size,
self.training,
)
batch_size = 32
num_features = 100
num_clusters = 32
feature_size = 512
ghost_clusters = 0
def get_inputs():
return [torch.randn(batch_size, num_features, feature_size)]
def get_init_inputs():
return [num_clusters, feature_size, ghost_clusters]
# Copyright 2018 Antoine Miech All Rights Reserved.
#
# Licensed 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.
"""
Code modified from here
https://github.com/albanie/collaborative-experts/blob/master/model/net_vlad.py
"""
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch as th
class Model(nn.Module):
def __init__(self, cluster_size, feature_size, ghost_clusters):
super(Model, self).__init__()
self.feature_size = feature_size
self.cluster_size = cluster_size
self.ghost_clusters = ghost_clusters
init_sc = (1 / math.sqrt(feature_size))
clusters = cluster_size + ghost_clusters
# The `clusters` weights are the `(w,b)` in the paper
self.clusters = nn.Parameter(init_sc * th.randn(feature_size, clusters))
self.batch_norm = nn.BatchNorm1d(clusters)
# The `clusters2` weights are the visual words `c_k` in the paper
self.clusters2 = nn.Parameter(init_sc * th.randn(1, feature_size, cluster_size))
self.out_dim = self.cluster_size * feature_size
def forward(self, x, mask=None):
"""Aggregates feature maps into a fixed size representation. In the following
notation, B = batch_size, N = num_features, K = num_clusters, D = feature_size.
Args:
x (th.Tensor): B x N x D
Returns:
(th.Tensor): B x DK
"""
max_sample = x.size()[1]
x = x.view(-1, self.feature_size) # B x N x D -> BN x D
if x.device != self.clusters.device:
msg = f"x.device {x.device} != cluster.device {self.clusters.device}"
raise ValueError(msg)
assignment = th.matmul(x, self.clusters) # (BN x D) x (D x (K+G)) -> BN x (K+G)
assignment = self.batch_norm(assignment)
assignment = F.softmax(assignment, dim=1) # BN x (K+G) -> BN x (K+G)
# remove ghost assigments
assignment = assignment[:, :self.cluster_size]
assignment = assignment.view(-1, max_sample, self.cluster_size) # -> B x N x K
a_sum = th.sum(assignment, dim=1, keepdim=True) # B x N x K -> B x 1 x K
a = a_sum * self.clusters2
assignment = assignment.transpose(1, 2) # B x N x K -> B x K x N
x = x.view(-1, max_sample, self.feature_size) # BN x D -> B x N x D
vlad = th.matmul(assignment, x) # (B x K x N) x (B x N x D) -> B x K x D
vlad = vlad.transpose(1, 2) # -> B x D x K
vlad = vlad - a
# L2 intra norm
vlad = F.normalize(vlad)
# flattening + L2 norm
vlad = vlad.reshape(-1, self.cluster_size * self.feature_size) # -> B x DK
vlad = F.normalize(vlad)
return vlad # B x DK
batch_size = 32
num_features = 100
num_clusters = 32
feature_size = 512
ghost_clusters = 0
def get_inputs():
return [torch.randn(batch_size, num_features, feature_size)]
def get_init_inputs():
return [num_clusters, feature_size, ghost_clusters]
#include <torch/extension.h>
#include <ATen/ATen.h>
#include <cuda.h>
#include <cuda_runtime.h>
#include <vector>
#define CHECK_CUDA(x) TORCH_CHECK(x.is_cuda(), #x " must be a CUDA tensor")
#define CHECK_CONTIGUOUS(x) TORCH_CHECK(x.is_contiguous(), #x " must be contiguous")
#define CHECK_INPUT(x) CHECK_CUDA(x); CHECK_CONTIGUOUS(x)
constexpr int TILE_SIZE = 128;
__global__ void fused_assignment_kernel(
const float* __restrict__ x,
const float* __restrict__ clusters,
const float* bn_weight,
const float* bn_bias,
const float* bn_mean,
const float* bn_var,
float* output,
int64_t BxN,
int64_t D,
int64_t KplusG,
bool is_training) {
int row = blockIdx.x * blockDim.y + threadIdx.y;
int tid = threadIdx.x;
int col = threadIdx.y;
__shared__ float smem_max[TILE_SIZE];
__shared__ float smem_sum[TILE_SIZE];
if (row >= BxN) return;
extern __shared__ float shared[];
float* row_cache = shared;
// Compute matmul row
float sum = 0.0f;
for (int i = tid; i < D; i += TILE_SIZE) {
sum += x[row * D + i] * clusters[i * KplusG + col];
}
atomicAdd(&row_cache[col], sum);
__syncthreads();
// Apply BN
float val = row_cache[col];
if (!is_training) {
val = (val - bn_mean[col]) * bn_weight[col] / sqrtf(bn_var[col] + 1e-5f) + bn_bias[col];
}
// Softmax reduction
float max_val = -INFINITY;
for (int i = tid; i < KplusG; i += TILE_SIZE)
max_val = fmaxf(max_val, row_cache[i]);
smem_max[tid] = max_val;
__syncthreads();
for (int s = blockDim.x/2; s > 0; s >>= 1) {
if (tid < s)
smem_max[tid] = fmaxf(smem_max[tid], smem_max[tid + s]);
__syncthreads();
}
max_val = smem_max[0];
float sum_exp = 0.0f;
val = __expf(val - max_val);
for (int i = tid; i < KplusG; i += TILE_SIZE)
sum_exp += row_cache[i];
smem_sum[tid] = sum_exp;
__syncthreads();
for (int s = blockDim.x/2; s > 0; s >>= 1) {
if (tid < s)
smem_sum[tid] += smem_sum[tid + s];
__syncthreads();
}
output[row * KplusG + col] = val / smem_sum[0];
}
torch::Tensor forward(
torch::Tensor x,
torch::Tensor clusters,
torch::Tensor clusters2,
torch::Tensor bn_weight,
torch::Tensor bn_bias,
torch::Tensor bn_running_mean,
torch::Tensor bn_running_var,
int64_t feature_size,
int64_t cluster_size,
bool is_training) {
CHECK_INPUT(x);
CHECK_INPUT(clusters);
CHECK_INPUT(clusters2);
CHECK_INPUT(bn_weight);
CHECK_INPUT(bn_bias);
CHECK_INPUT(bn_running_mean);
CHECK_INPUT(bn_running_var);
int64_t B = x.size(0);
int64_t N = x.size(1);
int64_t D = feature_size;
int64_t K = cluster_size;
int64_t KplusG = clusters.size(1);
int64_t BxN = B * N;
x = x.reshape({-1, D});
auto assignment = torch::empty({BxN, KplusG}, x.options());
dim3 block(TILE_SIZE, TILE_SIZE);
dim3 grid((BxN + TILE_SIZE - 1) / TILE_SIZE);
size_t shared_mem = KplusG * sizeof(float);
fused_assignment_kernel<<<grid, block, shared_mem>>>(
x.data_ptr<float>(),
clusters.data_ptr<float>(),
bn_weight.data_ptr<float>(),
bn_bias.data_ptr<float>(),
bn_running_mean.data_ptr<float>(),
bn_running_var.data_ptr<float>(),
assignment.data_ptr<float>(),
BxN,
D,
KplusG,
is_training);
assignment = assignment.narrow(1, 0, K).reshape({B, N, K});
auto a_sum = assignment.sum(1, true);
clusters2 = clusters2.expand({B, D, K});
auto a = clusters2 * a_sum;
assignment = assignment.transpose(1, 2);
x = x.reshape({B, N, D});
auto vlad = torch::bmm(assignment, x).transpose(1, 2) - a;
vlad = torch::nn::functional::normalize(
vlad, torch::nn::functional::NormalizeFuncOptions().p(2).dim(1));
vlad = vlad.reshape({B, D * K});
vlad = torch::nn::functional::normalize(
vlad, torch::nn::functional::NormalizeFuncOptions().p(2).dim(1));
return vlad;
}
PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
m.def("forward", &forward, "NetVLAD forward");
}
Metric | Value | Unit | Variance | Samples |
---|
Rule | Description |
---|
Operation / Metric | Value | Unit |
---|---|---|
aten::zero_ | ||
CPU Time | 129472.95 | μs |
Device Time | 1199898.81 | μs |
Self CPU Time | 27159.71 | μs |
Self Device Time | 0.00 | μs |
CPU Memory Usage | 0 | B |
Device Memory Usage | 0 | B |
Self CPU Memory Usage | 0 | B |
Self Device Memory Usage | 0 | B |
aten::fill_ | ||
CPU Time | 102347.92 | μs |
Device Time | 1199898.81 | μs |
Self CPU Time | 35045.24 | μs |
Self Device Time | 1199898.81 | μs |
CPU Memory Usage | 0 | B |
Device Memory Usage | 0 | B |
Self CPU Memory Usage | 0 | B |
Self Device Memory Usage | 0 | B |
aten::reshape | ||
CPU Time | 315712.37 | μs |
Device Time | 86664.34 | μs |
Self CPU Time | 69263.21 | μs |
Self Device Time | 0.00 | μs |
CPU Memory Usage | 0 | B |
Device Memory Usage | 0 | B |
Self CPU Memory Usage | 0 | B |
Self Device Memory Usage | 0 | B |
cudaLaunchKernel | ||
CPU Time | 796635.10 | μs |
Device Time | 15483.57 | μs |
Self CPU Time | 796635.10 | μs |
Self Device Time | 15483.57 | μs |
CPU Memory Usage | 0 | B |
Device Memory Usage | 0 | B |
Self CPU Memory Usage | 0 | B |
Self Device Memory Usage | 0 | B |
aten::bmm | ||
CPU Time | 340092.34 | μs |
Device Time | 145130.82 | μs |
Self CPU Time | 231769.44 | μs |
Self Device Time | 145130.82 | μs |
CPU Memory Usage | 0 | B |
Device Memory Usage | 0 | B |
Self CPU Memory Usage | 0 | B |
Self Device Memory Usage | 0 | B |
aten::norm | ||
CPU Time | 412794.28 | μs |
Device Time | 169358.29 | μs |
Self CPU Time | 123158.48 | μs |
Self Device Time | 0.00 | μs |
CPU Memory Usage | 0 | B |
Device Memory Usage | 0 | B |
Self CPU Memory Usage | 0 | B |
Self Device Memory Usage | 0 | B |
aten::linalg_vector_norm | ||
CPU Time | 289635.80 | μs |
Device Time | 169358.29 | μs |
Self CPU Time | 131194.65 | μs |
Self Device Time | 169358.29 | μs |
CPU Memory Usage | 0 | B |
Device Memory Usage | 0 | B |
Self CPU Memory Usage | 0 | B |
Self Device Memory Usage | 0 | B |
void at::native::vectorized_elementwise_kernel<4, at::native::FillFunctor<int>, at::detail::Array<char*, 1> >(int, at::native::FillFunctor<int>, at::detail::Array<char*, 1>) | ||
CPU Time | 0.00 | μs |
Device Time | 1199898.81 | μs |
Self CPU Time | 0.00 | μs |
Self Device Time | 1199898.81 | μs |
CPU Memory Usage | 0 | B |
Device Memory Usage | 0 | B |
Self CPU Memory Usage | 0 | B |
Self Device Memory Usage | 0 | B |
45297 warnings generated when compiling for host. Suppressed 45330 warnings (45283 in non-user code, 47 NOLINT). Use -header-filter=.* to display errors from all non-system headers. Use -system-headers to display errors from system headers as well.