Files
dmlc--dgl/src/array/cpu/labor_pick.h
T
2026-07-13 13:35:51 +08:00

321 lines
12 KiB
C++

/**
* Copyright (c) 2022, NVIDIA Corporation
* Copyright (c) 2022, GT-TDAlab (Muhammed Fatih Balin & Umit V. Catalyurek)
* 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.
*
* @file array/cpu/labor_pick.h
* @brief Template implementation for layerwise pick operators.
*/
#ifndef DGL_ARRAY_CPU_LABOR_PICK_H_
#define DGL_ARRAY_CPU_LABOR_PICK_H_
#include <dgl/array.h>
#include <dgl/random.h>
#include <dgl/runtime/parallel_for.h>
#include <dmlc/omp.h>
#include <tsl/robin_map.h>
#include <algorithm>
#include <cmath>
#include <functional>
#include <memory>
#include <numeric>
#include <string>
#include <utility>
#include <vector>
#include "../../random/continuous_seed.h"
namespace dgl {
namespace aten {
namespace impl {
using dgl::random::continuous_seed;
template <typename K, typename V>
using map_t = tsl::robin_map<K, V>;
template <typename iterator>
auto& mutable_value_ref(iterator it) {
return it.value();
}
constexpr double eps = 0.0001;
template <typename IdxType, typename FloatType>
auto compute_importance_sampling_probabilities(
DGLContext ctx, DGLDataType dtype, const IdxType max_degree,
const IdxType num_rows, const int importance_sampling, const bool weighted,
const IdxType* rows_data, const IdxType* indptr, const FloatType* A,
const IdxType* indices, const IdxType num_picks, const FloatType* ds,
FloatType* cs) {
constexpr FloatType ONE = 1;
// ps stands for \pi in arXiv:2210.13339
FloatArray ps_array = NDArray::Empty({max_degree + 1}, dtype, ctx);
FloatType* ps = ps_array.Ptr<FloatType>();
double prev_ex_nodes = max_degree * num_rows;
map_t<IdxType, FloatType> hop_map, hop_map2;
for (int iters = 0; iters < importance_sampling || importance_sampling < 0;
iters++) {
// NOTE(mfbalin) When the graph is unweighted, the first c values in
// the first iteration can be computed in O(1) as k / d where k is fanout
// and d is the degree.
// If the graph is weighted, the first c values are computed in the inner
// for loop instead. Therefore the importance_sampling argument should be
// increased by one in the caller.
// The later iterations will have correct c values so the if block will be
// executed.
if (!weighted || iters) {
hop_map2.clear();
for (int64_t i = 0; i < num_rows; ++i) {
const FloatType c = cs[i];
const IdxType rid = rows_data[i];
for (auto j = indptr[rid]; j < indptr[rid + 1]; j++) {
const auto ct = c * (weighted && iters == 1 ? A[j] : 1);
auto itb = hop_map2.emplace(indices[j], ct);
if (!itb.second) {
mutable_value_ref(itb.first) = std::max(ct, itb.first->second);
}
}
}
if (hop_map.empty())
hop_map = std::move(hop_map2);
else
// Update the pi array according to Eq 18.
for (auto it : hop_map2) hop_map[it.first] *= it.second;
}
// Compute c_s according to Equation (15), (17) is slower because sorting is
// required.
for (int64_t i = 0; i < num_rows; ++i) {
const IdxType rid = rows_data[i];
const auto d = indptr[rid + 1] - indptr[rid];
if (d == 0) continue;
const auto k = std::min(num_picks, d);
if (hop_map.empty()) { // weighted first iter, pi = A
for (auto j = indptr[rid]; j < indptr[rid + 1]; j++)
ps[j - indptr[rid]] = A[j];
} else {
for (auto j = indptr[rid]; j < indptr[rid + 1]; j++)
ps[j - indptr[rid]] = hop_map[indices[j]];
}
// stands for RHS of Equation (22) in arXiv:2210.13339 after moving the
// other terms without c_s to RHS.
double var_target = ds[i] * ds[i] / k;
if (weighted) {
var_target -= ds[i] * ds[i] / d;
for (auto j = indptr[rid]; j < indptr[rid + 1]; j++)
var_target += A[j] * A[j];
}
FloatType c = cs[i];
// stands for left handside of Equation (22) in arXiv:2210.13339 after
// moving the other terms without c_s to RHS.
double var_1;
// Compute c_s in Equation (22) via fixed-point iteration.
do {
var_1 = 0;
if (weighted) {
for (auto j = indptr[rid]; j < indptr[rid + 1]; j++)
// The check for zero is necessary for numerical stability
var_1 += A[j] > 0
? A[j] * A[j] / std::min(ONE, c * ps[j - indptr[rid]])
: 0;
} else {
for (auto j = indptr[rid]; j < indptr[rid + 1]; j++)
var_1 += ONE / std::min(ONE, c * ps[j - indptr[rid]]);
}
c *= var_1 / var_target;
} while (std::min(var_1, var_target) / std::max(var_1, var_target) <
1 - eps);
cs[i] = c;
}
// Check convergence
if (!weighted || iters) {
double cur_ex_nodes = 0;
for (auto it : hop_map) cur_ex_nodes += std::min((FloatType)1, it.second);
if (cur_ex_nodes / prev_ex_nodes >= 1 - eps) break;
prev_ex_nodes = cur_ex_nodes;
}
}
return hop_map;
}
// Template for picking non-zero values row-wise.
template <typename IdxType, typename FloatType>
std::pair<COOMatrix, FloatArray> CSRLaborPick(
CSRMatrix mat, IdArray rows, int64_t num_picks, FloatArray prob,
int importance_sampling, IdArray random_seed_arr, float seed2_contribution,
IdArray NIDs) {
using namespace aten;
const IdxType* indptr = mat.indptr.Ptr<IdxType>();
const IdxType* indices = mat.indices.Ptr<IdxType>();
const IdxType* data = CSRHasData(mat) ? mat.data.Ptr<IdxType>() : nullptr;
const IdxType* rows_data = rows.Ptr<IdxType>();
const IdxType* nids = IsNullArray(NIDs) ? nullptr : NIDs.Ptr<IdxType>();
const auto num_rows = rows->shape[0];
const auto& ctx = mat.indptr->ctx;
const bool weighted = !IsNullArray(prob);
// O(1) c computation not possible, so one more iteration is needed.
if (importance_sampling >= 0) importance_sampling += weighted;
// A stands for the same notation in arXiv:2210.13339, i.e. the edge weights.
auto A_arr = prob;
FloatType* A = A_arr.Ptr<FloatType>();
constexpr FloatType ONE = 1;
constexpr auto dtype = DGLDataTypeTraits<FloatType>::dtype;
// cs stands for c_s in arXiv:2210.13339
FloatArray cs_array = NDArray::Empty({num_rows}, dtype, ctx);
FloatType* cs = cs_array.Ptr<FloatType>();
// ds stands for A_{*s} in arXiv:2210.13339
FloatArray ds_array = NDArray::Empty({num_rows}, dtype, ctx);
FloatType* ds = ds_array.Ptr<FloatType>();
IdxType max_degree = 1;
IdxType hop_size = 0;
for (int64_t i = 0; i < num_rows; ++i) {
const IdxType rid = rows_data[i];
const auto act_degree = indptr[rid + 1] - indptr[rid];
max_degree = std::max(act_degree, max_degree);
double d = weighted
? std::accumulate(A + indptr[rid], A + indptr[rid + 1], 0.0)
: act_degree;
// O(1) c computation, samples more than needed for weighted case, mentioned
// in the sentence between (10) and (11) in arXiv:2210.13339
cs[i] = num_picks / d;
ds[i] = d;
hop_size += act_degree;
}
map_t<IdxType, FloatType> hop_map;
if (importance_sampling)
hop_map = compute_importance_sampling_probabilities<IdxType, FloatType>(
ctx, dtype, max_degree, num_rows, importance_sampling, weighted,
rows_data, indptr, A, indices, (IdxType)num_picks, ds, cs);
constexpr auto vidtype = DGLDataTypeTraits<IdxType>::dtype;
IdArray picked_row = NDArray::Empty({hop_size}, vidtype, ctx);
IdArray picked_col = NDArray::Empty({hop_size}, vidtype, ctx);
IdArray picked_idx = NDArray::Empty({hop_size}, vidtype, ctx);
FloatArray picked_imp = importance_sampling
? NDArray::Empty({hop_size}, dtype, ctx)
: NullArray();
IdxType* picked_rdata = picked_row.Ptr<IdxType>();
IdxType* picked_cdata = picked_col.Ptr<IdxType>();
IdxType* picked_idata = picked_idx.Ptr<IdxType>();
FloatType* picked_imp_data = picked_imp.Ptr<FloatType>();
const continuous_seed random_seed =
IsNullArray(random_seed_arr)
? continuous_seed(RandomEngine::ThreadLocal()->RandInt(1000000000))
: continuous_seed(random_seed_arr, seed2_contribution);
// compute number of edges first and do sampling
IdxType num_edges = 0;
for (int64_t i = 0; i < num_rows; i++) {
const IdxType rid = rows_data[i];
const auto c = cs[i];
FloatType norm_inv_p = 0;
const auto off = num_edges;
for (auto j = indptr[rid]; j < indptr[rid + 1]; j++) {
const auto v = indices[j];
const uint64_t t = nids ? nids[v] : v; // t in the paper
// rolled random number r_t is a function of the random_seed and t
const auto rnd = random_seed.uniform(t);
const auto w = (weighted ? A[j] : 1);
// if hop_map is initialized, get ps from there, otherwise get it from the
// alternative.
const auto ps = std::min(
ONE, importance_sampling - weighted ? c * hop_map[v] : c * w);
if (rnd <= ps) {
picked_rdata[num_edges] = rid;
picked_cdata[num_edges] = v;
picked_idata[num_edges] = data ? data[j] : j;
if (importance_sampling) {
const auto edge_weight = w / ps;
norm_inv_p += edge_weight;
picked_imp_data[num_edges] = edge_weight;
}
num_edges++;
}
}
if (importance_sampling) {
const auto norm_factor = (num_edges - off) / norm_inv_p;
for (auto i = off; i < num_edges; i++)
// so that fn.mean can be used
picked_imp_data[i] *= norm_factor;
}
}
picked_row = picked_row.CreateView({num_edges}, picked_row->dtype);
picked_col = picked_col.CreateView({num_edges}, picked_col->dtype);
picked_idx = picked_idx.CreateView({num_edges}, picked_idx->dtype);
if (importance_sampling)
picked_imp = picked_imp.CreateView({num_edges}, picked_imp->dtype);
return std::make_pair(
COOMatrix(mat.num_rows, mat.num_cols, picked_row, picked_col, picked_idx),
picked_imp);
}
// Template for picking non-zero values row-wise. The implementation first
// slices out the corresponding rows and then converts it to CSR format. It then
// performs row-wise pick on the CSR matrix and rectifies the returned results.
template <typename IdxType, typename FloatType>
std::pair<COOMatrix, FloatArray> COOLaborPick(
COOMatrix mat, IdArray rows, int64_t num_picks, FloatArray prob,
int importance_sampling, IdArray random_seed, float seed2_contribution,
IdArray NIDs) {
using namespace aten;
const auto& csr = COOToCSR(COOSliceRows(mat, rows));
const IdArray new_rows =
Range(0, rows->shape[0], rows->dtype.bits, rows->ctx);
const auto&& picked_importances = CSRLaborPick<IdxType, FloatType>(
csr, new_rows, num_picks, prob, importance_sampling, random_seed,
seed2_contribution, NIDs);
const auto& picked = picked_importances.first;
const auto& importances = picked_importances.second;
return std::make_pair(
COOMatrix(
mat.num_rows, mat.num_cols,
IndexSelect(
rows, picked.row), // map the row index to the correct one
picked.col, picked.data),
importances);
}
} // namespace impl
} // namespace aten
} // namespace dgl
#endif // DGL_ARRAY_CPU_LABOR_PICK_H_