diff --git a/CMakeLists.txt b/CMakeLists.txt index 95e90896..cc995ff9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ project(GraphZeppelin) include (FetchContent) -set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS ON) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib) @@ -21,7 +21,7 @@ message(STATUS "GraphZeppelin Build Type: ${CMAKE_BUILD_TYPE}") if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") message(STATUS "Adding GNU compiler flags") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -W -Wall") -elseif(STATUS "${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC") +elseif("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC") message("Adding MSVC compiler flags") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /Wall") else() @@ -59,6 +59,22 @@ FetchContent_Declare( GIT_TAG main ) +# Get VieCut +FetchContent_Declare( + VieCut + + GIT_REPOSITORY https://github.com/etwest/VieCut.git + GIT_TAG master +) + +# Get tlx +FetchContent_Declare( + tlx + + GIT_REPOSITORY https://github.com/tlx/tlx.git + GIT_TAG master +) + if (BUILD_BENCH) # Get Google Benchmark FetchContent_Declare( @@ -72,29 +88,35 @@ if (BUILD_BENCH) FetchContent_MakeAvailable(benchmark) endif() -FetchContent_MakeAvailable(GutterTree StreamingUtilities) +FetchContent_MakeAvailable(GutterTree StreamingUtilities VieCut tlx) # AVAILABLE COMPILATION DEFINITIONS: -# VERIFY_SAMPLES_F Use a deterministic connected-components -# algorithm to verify post-processing. # NO_EAGER_DSU Do not use the eager DSU query optimization # if this flag is present. # L0_SAMPLING Run the CubeSketch l0 sampling algorithm # to ensure that we sample uniformly. # Otherwise, run a support finding algorithm. +# L0_FULLY_DENSE Fully allocate the sketch matrix at the beginning +# of the program. If this flag is not used, sketches +# are allocated dynamically. +# VERIFY_SAMPLES_F Use a deterministic connected-components +# algorithm to verify post-processing. # # Example: # cmake -DCMAKE_CXX_FLAGS="-DL0_SAMPLING" .. add_library(GraphZeppelin src/cc_sketch_alg.cpp + src/edge_store.cpp + src/min_cut_sketch_alg.cpp src/return_types.cpp src/driver_configuration.cpp src/cc_alg_configuration.cpp - src/sketch.cpp + src/sparse_sketch.cpp + src/dense_sketch.cpp src/util.cpp) -add_dependencies(GraphZeppelin GutterTree StreamingUtilities) -target_link_libraries(GraphZeppelin PUBLIC xxhash GutterTree StreamingUtilities) +add_dependencies(GraphZeppelin GutterTree StreamingUtilities VieCut tlx) +target_link_libraries(GraphZeppelin PUBLIC xxhash GutterTree StreamingUtilities VieCut tlx) target_include_directories(GraphZeppelin PUBLIC include/) target_compile_options(GraphZeppelin PUBLIC -fopenmp) target_link_options(GraphZeppelin PUBLIC -fopenmp) @@ -102,14 +124,17 @@ target_compile_definitions(GraphZeppelin PUBLIC XXH_INLINE_ALL) add_library(GraphZeppelinVerifyCC src/cc_sketch_alg.cpp + src/edge_store.cpp + src/min_cut_sketch_alg.cpp src/return_types.cpp src/driver_configuration.cpp src/cc_alg_configuration.cpp - src/sketch.cpp + src/sparse_sketch.cpp + src/dense_sketch.cpp src/util.cpp test/util/graph_verifier.cpp) -add_dependencies(GraphZeppelinVerifyCC GutterTree StreamingUtilities) -target_link_libraries(GraphZeppelinVerifyCC PUBLIC xxhash GutterTree StreamingUtilities) +add_dependencies(GraphZeppelinVerifyCC GutterTree StreamingUtilities VieCut tlx) +target_link_libraries(GraphZeppelinVerifyCC PUBLIC xxhash GutterTree StreamingUtilities VieCut tlx) target_include_directories(GraphZeppelinVerifyCC PUBLIC include/ include/test/) target_compile_options(GraphZeppelinVerifyCC PUBLIC -fopenmp) target_link_options(GraphZeppelinVerifyCC PUBLIC -fopenmp) @@ -119,6 +144,8 @@ if (BUILD_EXE) add_executable(tests test/test_runner.cpp test/cc_alg_test.cpp + test/min_cut_test.cpp + test/edge_store_test.cpp test/sketch_test.cpp test/dsu_test.cpp test/util_test.cpp @@ -137,6 +164,11 @@ if (BUILD_EXE) tools/process_stream.cpp) target_link_libraries(process_stream PRIVATE GraphZeppelin) + # executable for processing a binary graph stream + add_executable(min_cut_exe + tools/minimum_cut.cpp) + target_link_libraries(min_cut_exe PRIVATE GraphZeppelin) + # executable for performing in depth correctness testing add_executable(test_correctness tools/test_correctness.cpp) diff --git a/include/bucket.h b/include/bucket.h index 5d6a6af6..87c1c22c 100644 --- a/include/bucket.h +++ b/include/bucket.h @@ -1,73 +1,94 @@ #pragma once -#include #include + #include +#include +#include + #include "types.h" -#pragma pack(push,1) +#pragma pack(push, 1) struct Bucket { vec_t alpha; vec_hash_t gamma; }; #pragma pack(pop) -namespace Bucket_Boruvka { - static constexpr size_t col_hash_bits = sizeof(col_hash_t) * 8; - /** - * Hashes the column index and the update index together to determine the depth of an update - * This is used as a parameter to Bucket::contains. - * @param update_idx Vector index to update - * @param seed_and_col Combination of seed and column - * @param max_depth The maximum depth to return - * @return The hash of update_idx using seed_and_col as a seed. - */ - inline static col_hash_t get_index_depth(const vec_t update_idx, const long seed_and_col, - const vec_hash_t max_depth); +namespace SketchBucket { - /** - * Hashes the index for checksumming - * This is used to as a parameter to Bucket::update - * @param index Vector index to update - * @param seed The seed of the Sketch this Bucket belongs to - * @return The depth of the bucket to update - */ - inline static vec_hash_t get_index_hash(const vec_t index, const long sketch_seed); - - /** - * Checks whether a Bucket is good, assuming the Bucket contains all elements. - * @param bucket The bucket to check - * @param sketch_seed The seed of the Sketch this Bucket belongs to. - * @return true if this Bucket is good, else false. - */ - inline static bool is_good(const Bucket &bucket, const long sketch_seed); - - /** - * Updates a Bucket with the given update index - * @param bucket The bucket to update - * @param update_idx The update index - * @param update_hash The hash of the update index, generated with Bucket::index_hash. - */ - inline static void update(Bucket& bucket, const vec_t update_idx, - const vec_hash_t update_hash); -} // namespace Bucket_Boruvka +struct Depths { + private: + col_hash_t depths[2]; + public: + col_hash_t& operator[](size_t i) { return depths[i]; } +}; -inline col_hash_t Bucket_Boruvka::get_index_depth(const vec_t update_idx, const long seed_and_col, - const vec_hash_t max_depth) { +static constexpr size_t col_hash_bits = sizeof(col_hash_t) * 8; +/** + * Hashes the column index and the update index together to determine the depth of an update + * This is used as a parameter to Bucket::contains. + * @param update_idx Vector index to update + * @param seed_and_col Combination of seed and column + * @param max_depth The maximum depth to return + * @return The hash of update_idx using seed_and_col as a seed. + */ +inline static col_hash_t get_index_depth(const vec_t update_idx, const long seed_and_col, + const vec_hash_t max_depth) { col_hash_t depth_hash = col_hash(&update_idx, sizeof(vec_t), seed_and_col); - depth_hash |= (1ull << max_depth); // assert not > max_depth by ORing + depth_hash |= (1ull << max_depth); // assert not > max_depth by ORing return __builtin_ctzll(depth_hash); } -inline vec_hash_t Bucket_Boruvka::get_index_hash(const vec_t update_idx, const long sketch_seed) { - return vec_hash(&update_idx, sizeof(vec_t), sketch_seed); +inline static Depths get_index_depths(vec_t update_idx, size_t seed, col_hash_t max_depth) { + uint64_t depth_hash = col_hash(&update_idx, sizeof(vec_t), seed); + Depths ret; + + // assert not > max_depth by ORing + ret[0] = __builtin_ctzll(depth_hash | (1ull << max_depth)); + + // shift hash over, reassert max_depth, and grab another depth + depth_hash >>= (ret[0] + 1); + ret[1] = __builtin_ctzll(depth_hash | (1ull << max_depth)); + + return ret; } -inline bool Bucket_Boruvka::is_good(const Bucket &bucket, const long sketch_seed) { +/** + * Hashes the index for checksumming + * This is used to as a parameter to Bucket::update + * @param index Vector index to update + * @param seed The seed of the Sketch this Bucket belongs to + * @return The depth of the bucket to update + */ +inline static vec_hash_t get_index_hash(const vec_t index, const long sketch_seed) { + return vec_hash(&index, sizeof(vec_t), sketch_seed); +} + +/** + * Checks whether a Bucket is good. + * @param bucket The bucket to check + * @param sketch_seed The seed of the Sketch this Bucket belongs to. + * @return true if this Bucket is good, else false. + */ +inline static bool is_good(const Bucket &bucket, const long sketch_seed) { return bucket.gamma == get_index_hash(bucket.alpha, sketch_seed); } -inline void Bucket_Boruvka::update(Bucket& bucket, const vec_t update_idx, - const vec_hash_t update_hash) { +/** + * Checks whether a Bucket is empty. + * @return true if this Bucket is empty (alpha and gamma == 0), else false. + */ +inline static bool is_empty(const Bucket &bucket) { return bucket.alpha == 0 && bucket.gamma == 0; } + +/** + * Updates a Bucket with the given update index + * @param bucket The bucket to update + * @param update_idx The update index + * @param update_hash The hash of the update index, generated with Bucket::index_hash. + */ +inline static void update(Bucket &bucket, const vec_t update_idx, const vec_hash_t update_hash) { bucket.alpha ^= update_idx; bucket.gamma ^= update_hash; } + +} // namespace SketchBucket diff --git a/include/cc_sketch_alg.h b/include/cc_sketch_alg.h index 9e9d3f8c..bbdd5ceb 100644 --- a/include/cc_sketch_alg.h +++ b/include/cc_sketch_alg.h @@ -60,6 +60,7 @@ struct alignas(64) GlobalMergeData { enum QueryCode { CONNECTIVITY, // connected components and spanning forest of graph KSPANNINGFORESTS, // k disjoint spanning forests + MINIMUMCUT, // minimum cut query }; /** @@ -71,12 +72,13 @@ class CCSketchAlg { node_id_t num_vertices; size_t seed; bool update_locked = false; - // a set containing one "representative" from each supernode - std::set *representatives; Sketch **sketches; // DSU representation of supernode relationship DisjointSetUnion_MT dsu; + // TODO: Remove this variable later + std::atomic num_updates; + // if dsu valid then we have a cached query answer. Additionally, we need to update the DSU in // pre_insert() bool dsu_valid = true; @@ -84,6 +86,7 @@ class CCSketchAlg { // for accessing if the DSU is valid from threads that do not perform updates std::atomic shared_dsu_valid; + // Most recent spanning forest computed by algorithm and associated locks std::unordered_set *spanning_forest; std::mutex *spanning_forest_mtx; @@ -105,8 +108,8 @@ class CCSketchAlg { /** * Sample a single supernode represented by a single sketch containing one or more vertices. * Updates the dsu and spanning forest with query results if edge contains new connectivity info. - * @param skt sketch to sample - * @return [bool] true if the query result indicates we should run an additional round. + * param: skt sketch to sample + * return: [bool] true if the query result indicates we should run an additional round. */ bool sample_supernode(Sketch &skt); @@ -116,18 +119,25 @@ class CCSketchAlg { void create_merge_instructions(std::vector &merge_instr); /** - * @param reps set containing the roots of each supernode - * @param merge_instr a list of lists of supernodes to be merged + * param: reps set containing the roots of each supernode + * param: merge_instr a list of lists of supernodes to be merged */ bool perform_boruvka_round(const size_t cur_round, const std::vector &merge_instr, std::vector &global_merges); /** - * Main parallel algorithm utilizing Boruvka and L_0 sampling. - * Ensures that the DSU represents the Connected Components of the stream when called + * Main parallel algorithm subroutine for query computation */ void boruvka_emulation(); + /** + * Ensures that the DSU represents the Connected Components of the stream when called + * and that spanning_forest is correct. + */ + void compute_dsu(); + + void filter_sf_edges(SpanningForest &sf); + // constructor for use when reading from a serialized file CCSketchAlg(node_id_t num_vertices, size_t seed, std::ifstream &binary_stream, CCAlgConfiguration config); @@ -170,13 +180,24 @@ class CCSketchAlg { /** * Update all the sketches for a node, given a batch of updates. - * @param thr_id The id of the thread performing the update [0, num_threads) - * @param src_vertex The vertex where the edges originate. - * @param dst_vertices A vector of destinations. + * param: thr_id The id of the thread performing the update [0, num_threads) + * param: src_vertex The vertex where the edges originate. + * param: dst_vertices A vector of destinations. */ void apply_update_batch(int thr_id, node_id_t src_vertex, const std::vector &dst_vertices); + /** + * Update the sketches for a particular vertex, given a batch of edge indices. These indices must + * be constructed using concat_pairing_fn() and must all be associated with a particular graph + * vertex. + * param: thr_id The id of the thread performing the update [0, num_threads) + * param: src_vertex The vertex where the edges originate. + * param: idxs A vector of concatenated edges. + */ + void apply_concat_update_batch(int thr_id, node_id_t src_vertex, + const std::vector &idxs); + /** * Return if we have cached an answer to query. * This allows the driver to avoid flushing the gutters before calling query functions. @@ -201,8 +222,9 @@ class CCSketchAlg { * Specifically, the delta is in the form of a pointer to raw bucket data. * @param src_vertex The vertex where the all edges originate. * @param raw_buckets Pointer to the array of buckets from the delta sketch + * @param num_buckets Size of raw_buckets array in number of buckets */ - void apply_raw_buckets_update(node_id_t src_vertex, Bucket *raw_buckets); + void apply_raw_buckets_update(node_id_t src_vertex, Bucket *raw_buckets, size_t num_buckets); /** * The function performs a direct update to the associated sketch. @@ -214,15 +236,15 @@ class CCSketchAlg { /** * Main parallel query algorithm utilizing Boruvka and L_0 sampling. - * @return the connected components in the graph. + * return: the connected components in the graph. */ ConnectedComponents connected_components(); /** * Point query algorithm utilizing Boruvka and L_0 sampling. * Allows for additional updates when done. - * @param a, b - * @return true if a and b are in the same connected component, false otherwise. + * param: a, b vertices of the graph. Check if these are connected. + * return: true if a and b are in the same connected component, false otherwise. */ bool point_query(node_id_t a, node_id_t b); @@ -230,10 +252,18 @@ class CCSketchAlg { * Return a spanning forest of the graph utilizing Boruvka and L_0 sampling * IMPORTANT: The updates to this algorithm MUST NOT be a function of the output of this query * that is, unless you really know what you're doing. - * @return the spanning forest of the graph + * return: the spanning forest of the graph */ SpanningForest calc_spanning_forest(); + /** + * Return k edge-disjoint spanning forests of the graph. + * IMPORTANT: The updates to this algorithm MUST NOT be a function of the output of this query + * that is, unless you really know what you're doing. + * return: k edge-disjoint spanning forests of the graph. + */ + std::vector calc_disjoint_spanning_forests(size_t k); + #ifdef VERIFY_SAMPLES_F void set_verifier(std::unique_ptr verifier) { this->verifier = std::move(verifier); @@ -242,7 +272,7 @@ class CCSketchAlg { /** * Serialize the graph data to a binary file. - * @param filename the name of the file to (over)write data to. + * param: filename the name of the file to (over)write data to. */ void write_binary(const std::string &filename); @@ -255,4 +285,10 @@ class CCSketchAlg { inline node_id_t get_num_vertices() { return num_vertices; } inline size_t get_seed() { return seed; } inline size_t max_rounds() { return sketches[0]->get_num_samples(); } + inline size_t get_num_updates() { return num_updates.load(); } + + void invalidate_dsu() { + dsu_valid = false; + shared_dsu_valid = false; + } }; diff --git a/include/dense_sketch.h b/include/dense_sketch.h new file mode 100644 index 00000000..5a6e4047 --- /dev/null +++ b/include/dense_sketch.h @@ -0,0 +1,191 @@ +#pragma once +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "util.h" +#include "bucket.h" +#include "sketch_types.h" + +class SparseSketch; + +/** + * Sketch for graph processing, either CubeSketch or CameoSketch. + * Sub-linear representation of a vector. + */ +class DenseSketch { + private: + const uint64_t seed; // seed for hash functions + size_t num_samples; // number of samples we can perform + size_t cols_per_sample; // number of columns to use on each sample + size_t num_columns; // Total number of columns. (product of above 2) + size_t bkt_per_col; // maximum number of buckets per column (max number of rows) + size_t num_buckets; // number of total buckets product of above two + size_t sample_idx = 0; // number of samples performed so far + + // Allocated buckets + Bucket* buckets; + + inline Bucket& deterministic_bucket() { + return buckets[0]; + } + inline const Bucket& deterministic_bucket() const { + return buckets[0]; + } + + // return the bucket at a particular index in bucket array + inline Bucket& bucket(size_t col, size_t row) { + return buckets[col * bkt_per_col + row + 1]; + } + inline const Bucket& bucket(size_t col, size_t row) const { + return buckets[col * bkt_per_col + row + 1]; + } + + public: + /** + * The below constructors use vector length as their input. However, in graph sketching our input + * is the number of vertices. This function converts from number of graph vertices to vector + * length. + * @param num_vertices Number of graph vertices + * @return The length of the vector to sketch + */ + static vec_t calc_vector_length(node_id_t num_vertices) { + return ceil(double(num_vertices) * (num_vertices - 1) / 2); + } + + /** + * This function computes the number of samples a Sketch should support in order to solve + * connected components. Optionally, can increase or decrease the number of samples by a + * multiplicative factor. + * @param num_vertices Number of graph vertices + * @param f Multiplicative sample factor + * @return The number of samples + */ + static size_t calc_cc_samples(node_id_t num_vertices, double f) { + return std::max(size_t(18), (size_t) ceil(f * log2(num_vertices) / num_samples_div)); + } + + /** + * Construct a sketch object + * @param vector_len Length of the vector we are sketching + * @param seed Random seed of the sketch + * @param num_samples [Optional] Number of samples this sketch supports (default = 1) + * @param cols_per_sample [Optional] Number of sketch columns for each sample (default = 1) + */ + DenseSketch(vec_t vector_len, uint64_t seed, size_t num_samples = 1, + size_t cols_per_sample = default_cols_per_sample); + + /** + * Construct a sketch from a serialized stream + * @param vector_len Length of the vector we are sketching + * @param seed Random seed of the sketch + * @param binary_in Stream holding serialized sketch object + * @param num_buckets Number of buckets in serialized sketch + * @param num_samples [Optional] Number of samples this sketch supports (default = 1) + * @param cols_per_sample [Optional] Number of sketch columns for each sample (default = 1) + */ + DenseSketch(vec_t vector_len, uint64_t seed, std::istream& binary_in, size_t num_buckets, + size_t num_samples = 1, size_t cols_per_sample = default_cols_per_sample); + + /** + * Sketch copy constructor + * @param s The sketch to copy. + */ + DenseSketch(const DenseSketch& s); + + ~DenseSketch(); + + /** + * Update a sketch based on information about one of its indices. + * @param update the point update. + */ + void update(const vec_t update); + + /** + * Function to sample from the sketch. + * cols_per_sample determines the number of columns we allocate to this query + * @return A pair with the result index and a code indicating the type of result. + */ + SketchSample sample(); + + /** + * Function to sample from the appropriate columns to return 1 or more non-zero indices + * @return A pair with the result indices and a code indicating the type of result. + */ + ExhaustiveSketchSample exhaustive_sample(); + + std::mutex mutex; // lock the sketch for applying updates in multithreaded processing + + /** + * In-place merge function. + * @param other Sketch to merge into caller + */ + void merge(const DenseSketch &other); + + /** + * In-place range merge function. Updates the caller Sketch. + * The range merge only merges some of the Sketches + * This function should only be used if you know what you're doing + * @param other Sketch to merge into caller + * @param start_sample Index of first sample to merge + * @param n_samples Number of samples to merge + */ + void range_merge(const DenseSketch &other, size_t start_sample, size_t n_samples); + + /** + * Perform an in-place merge function without another Sketch and instead + * use a raw bucket memory. + * We also allow for only a portion of the buckets to be merge at once + * @param raw_bucket Raw bucket data to merge into this sketch + * @param n_raw_buckets Size of raw_buckets in number of Bucket data-structures + */ + void merge_raw_bucket_buffer(const Bucket *raw_buckets, size_t n_raw_buckets); + + /** + * Zero out all the buckets of a sketch. + */ + void zero_contents(); + + friend bool operator==(const DenseSketch& sketch1, const DenseSketch& sketch2); + friend bool operator==(const SparseSketch& sparse, const DenseSketch& dense); + friend std::ostream& operator<<(std::ostream& os, const DenseSketch& sketch); + + /** + * Serialize the sketch to a binary output stream. + * @param binary_out the stream to write to. + */ + void serialize(std::ostream& binary_out) const; + + inline void reset_sample_state() { + sample_idx = 0; + } + + // return the size of the sketching datastructure in bytes (just the buckets, not the metadata) + inline size_t bucket_array_bytes() const { + return num_buckets * sizeof(Bucket); + } + + // return the size of a sketch given vector size n and number of samples s + static size_t estimate_bytes(size_t n, size_t s) { + return (1 + calc_bkt_per_col(n) * s * default_cols_per_sample) * sizeof(Bucket); + } + + inline const Bucket* get_readonly_bucket_ptr() const { return (const Bucket*) buckets; } + inline uint64_t get_seed() const { return seed; } + inline size_t column_seed(size_t column_idx) const { return seed + (5 * column_idx); } + inline size_t checksum_seed() const { return seed; } + inline size_t get_columns() const { return num_columns; } + inline size_t get_buckets() const { return num_buckets; } + inline size_t get_num_samples() const { return num_samples; } + + static size_t calc_bkt_per_col(size_t n) { return ceil(log2(n)) + 1; } + + static constexpr size_t default_cols_per_sample = 1; + static constexpr double num_samples_div = 1 - log2(2 - 0.8); +}; diff --git a/include/driver_configuration.h b/include/driver_configuration.h index e7e0dde5..cf076fbc 100644 --- a/include/driver_configuration.h +++ b/include/driver_configuration.h @@ -17,9 +17,12 @@ class DriverConfiguration { // Where to place on-disk datastructures std::string _disk_dir = "."; - // The number of worker threads + // The number of worker threads. These perform the algorithm updates. size_t _num_worker_threads = 1; + // The number of threads that read from the stream + size_t _num_stream_threads = 1; + // Configuration for the guttering system GutteringConfiguration _gutter_conf; @@ -29,13 +32,15 @@ class DriverConfiguration { // setters DriverConfiguration& gutter_sys(GutterSystem gutter_sys); DriverConfiguration& disk_dir(std::string disk_dir); - DriverConfiguration& worker_threads(size_t num_groups); + DriverConfiguration& worker_threads(size_t num_threads); + DriverConfiguration& stream_threads(size_t num_threads); GutteringConfiguration& gutter_conf(); // getters GutterSystem get_gutter_sys() { return _gutter_sys; } std::string get_disk_dir() { return _disk_dir; } size_t get_worker_threads() { return _num_worker_threads; } + size_t get_stream_threads() { return _num_stream_threads; } friend std::ostream& operator<< (std::ostream &out, const DriverConfiguration &conf); diff --git a/include/edge_store.h b/include/edge_store.h new file mode 100644 index 00000000..54164ddc --- /dev/null +++ b/include/edge_store.h @@ -0,0 +1,81 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "types.h" + +class EdgeStore { + private: + static constexpr size_t store_edge_bytes = sizeof(SubgraphTaggedUpdate); // Bytes of one edge + static constexpr double contract_factor = 2; // switch to sketch when within this factor of max + + size_t seed; + node_id_t num_vertices; + size_t num_subgraphs; + volatile size_t cur_subgraph = 0; // subgraph depth at which edges enter the edge store + volatile size_t true_min_subgraph = 0; // the minimum subgraph of elements in the store + + std::atomic num_edges; + std::atomic needs_contraction; + + std::vector> adjlist; + + // This is a vector of booleans BUT we don't want to use vector because its not + // multithread friendly + std::vector vertex_contracted; + + size_t max_edges; // Bytes of sketch graph + size_t default_buffer_allocation; // size we allocate each buffer in adjlist to + + // locks that protect the adjacency list + // we have a single lock for each vertex and a lock for handling contraction logic + std::mutex* adj_mutex; + std::mutex contract_lock; + + std::vector vertex_contract(node_id_t src); + void check_if_too_big(); + +#ifdef VERIFY_SAMPLES_F + void verify_contract_complete(); + std::atomic num_inserted; + std::atomic num_duplicate; + std::atomic num_returned; +#endif + public: + /** + * EdgeStore Constructor + * param: seed seed to depth hash function. (MUST be the same as the MinCutAlgorithm's) + * param: num_vertices number of vertices in the graph + * param: sketch_bytes number of bytes per vertex in sketch data-structure + * param: num_subgraphs maximum number of subgraphs + * param: start_subgraph Number of subgraphs we start with sketches for (updates don't go here) + */ + EdgeStore(size_t seed, node_id_t num_vertices, size_t sketch_bytes, size_t num_subgraphs, + size_t start_subgraph = 0); + ~EdgeStore(); + + // functions for adding data to the edge store + // may return a vector of edges that need to be applied to + + // this first function is only called when there exist no sketch subgraphs + TaggedUpdateBatch insert_adj_edges(node_id_t src, const std::vector& dst_vertices); + + // this function is called when there are some sketch subgraphs. + TaggedUpdateBatch insert_adj_edges(node_id_t src, node_id_t caller_first_es_subgraph, + std::vector &dst_data); + + // contract vertex data by removing all updates bound for lower subgraphs than the store + // is responsible for + TaggedUpdateBatch vertex_advance_subgraph(node_id_t cur_first_es_subgraph); + + // Get methods + size_t get_num_edges() { return num_edges; } + size_t get_footprint() { return num_edges * store_edge_bytes; } + size_t get_first_store_subgraph() { return cur_subgraph; } + std::vector get_edges(); + bool contract_in_progress() { return true_min_subgraph < cur_subgraph; } +}; diff --git a/include/graph_sketch_driver.h b/include/graph_sketch_driver.h index e627e443..2d490bb2 100644 --- a/include/graph_sketch_driver.h +++ b/include/graph_sketch_driver.h @@ -85,9 +85,10 @@ class GraphSketchDriver { std::atomic total_updates; public: - GraphSketchDriver(Alg *sketching_alg, GraphStream *stream, DriverConfiguration config, - size_t num_stream_threads = 1) - : sketching_alg(sketching_alg), stream(stream), num_stream_threads(num_stream_threads) { + GraphSketchDriver(Alg *sketching_alg, GraphStream *stream, DriverConfiguration config) + : sketching_alg(sketching_alg), + stream(stream), + num_stream_threads(config.get_stream_threads()) { sketching_alg->allocate_worker_memory(config.get_worker_threads()); // set the leaf size of the guttering system appropriately if (config.gutter_conf().get_gutter_bytes() == GutteringConfiguration::uninit_param) { @@ -140,8 +141,8 @@ class GraphSketchDriver { */ void process_stream_until(edge_id_t break_edge_idx) { if (!stream->set_break_point(break_edge_idx)) { - DriverException("Could not correctly set breakpoint: " + std::to_string(break_edge_idx)); - exit(EXIT_FAILURE); + throw DriverException("Could not correctly set breakpoint: " + + std::to_string(break_edge_idx)); } worker_threads->resume_workers(); diff --git a/include/mc_configuration.h b/include/mc_configuration.h new file mode 100644 index 00000000..a599c3d5 --- /dev/null +++ b/include/mc_configuration.h @@ -0,0 +1,61 @@ +#pragma once +#include + +// Configuration options for the minimum cut sketch algorithm +class MCAlgConfiguration { + private: + // How large to make update batches as factor of sketch size + double _batch_factor = 1; + + // Returned min-cut guaranteed to be a +/- epsilon multiplicative approx of the true min cut. + double _epsilon = 0.5; + + // Number of sketch subgraphs to create immediately. + // Preallocating these sketches improves performance at the cost of additional memory consumption + // if you already that you will need more (say 5) then setting this to the higher value is good + size_t _initial_subgraphs = 1; + + friend class MinCutSketchAlg; + public: + // setters + MCAlgConfiguration& batch_factor(double batch_factor) { + if (batch_factor <= 0) { + std::cerr << "WARNING: MCAlgConfiguration, batch factor must be > 0." << std::endl; + std::cerr << " Setting to default value: " << _batch_factor << std::endl; + } else { + _batch_factor = batch_factor; + } + return *this; + } + MCAlgConfiguration& epsilon(double epsilon) { + if (epsilon <= 0 || epsilon > 1) { + std::cerr << "WARNING: MCAlgConfiguration epsilon must be in range (0, 1]." << std::endl; + std::cerr << " Setting to default value: " << _epsilon << std::endl; + } else { + _epsilon = epsilon; + } + return *this; + } + MCAlgConfiguration& initial_subgraphs(size_t num_subgraphs) { + if (num_subgraphs == 0) { + std::cerr << "WARNING: MCAlgConfiguration, initial subgraphs must be > 0." << std::endl; + std::cerr << " Setting to default value: " << _initial_subgraphs << std::endl; + } else { + _initial_subgraphs = num_subgraphs; + } + return *this; + } + + // getters + double get_batch_factor() { return _batch_factor; } + double get_epsilon() { return _epsilon; } + size_t get_initial_subgraphs() { return _initial_subgraphs; } + + friend std::ostream& operator<< (std::ostream &out, const MCAlgConfiguration &conf) { + out << "Minimum Cut Algorithm Configuration:" << std::endl; + out << " batch_factor = " << conf._batch_factor << std::endl; + out << " epsilon = " << conf._epsilon << std::endl; + out << " initial_sketch_subgraphs = " << conf._initial_subgraphs << std::endl; + return out; + } +}; diff --git a/include/min_cut_sketch_alg.h b/include/min_cut_sketch_alg.h new file mode 100644 index 00000000..2e165679 --- /dev/null +++ b/include/min_cut_sketch_alg.h @@ -0,0 +1,130 @@ +#pragma once +#include +#include +#include + +#include "cc_sketch_alg.h" +#include "edge_store.h" +#include "mc_configuration.h" + +// Minimum cut sketch algorithm class +class MinCutSketchAlg { + private: + struct ThreadData { + std::vector> cc_buffers; + std::vector edge_store_buffer; + }; + + const node_id_t num_vertices; + const size_t seed; + const size_t subgraph_seed; + MCAlgConfiguration config; + const size_t max_subgraphs; + const size_t k; + std::atomic cur_subgraphs; + std::mutex advance_subgraph_lock; + + const double sketch_factor; + const size_t sketch_samples; + const size_t buffer_elms; + + CCSketchAlg **cc_sketches; + EdgeStore edge_store; + + Sketch *delta_sketches = nullptr; + ThreadData *thread_data = nullptr; + size_t num_delta_sketches = 0; + size_t num_workers; + +#ifdef VERIFY_SAMPLES_F + std::unique_ptr verifier; + std::unique_ptr adj_verifier; +#endif + + CCAlgConfiguration cc_config; + + void advance_cur_subgraph(size_t new_cur_subgraphs); + + void create_subgraph_verifiers(); + public: + /** + * Construct an instance of the Minimum Cut Sketching Algorithm + * param _num_vertices number of graph vertices + * param _seed seed to hash functions + * param _config Configuration options for minimum cut sketch algorithm + */ + MinCutSketchAlg(node_id_t _num_vertices, size_t _seed, + MCAlgConfiguration _config = MCAlgConfiguration()); + + ~MinCutSketchAlg(); + + /** + * Allocate memory for the worker threads to use when updating this algorithm's sketches + */ + void allocate_worker_memory(size_t num_workers); + + /** + * Returns the number of buffered updates we would like to have in the update batches + */ + size_t get_desired_updates_per_batch() { + return cc_sketches[0]->get_desired_updates_per_batch(); + } + + /** + * Action to take on an update before inserting it to the guttering system. + * We use this function to manage the eager dsu. + */ + void pre_insert(GraphUpdate upd, node_id_t thr_id); + + + /** + * Update all the sketches for a vertex, given a batch of updates. + * param thr_id The id of the thread performing the update [0, num_threads) + * param src_vertex The vertex where the edges originate. + * param dst_vertices A vector of destinations. + */ + void apply_update_batch(size_t thr_id, node_id_t src_vertex, + const std::vector &dst_vertices); + + /** + * Set the verifier this algorithm will use to check its correctness + * param: _verifier the verifier to use, should contain all edges processed at this point + */ +#ifdef VERIFY_SAMPLES_F + void set_verifier(std::unique_ptr _verifier); +#endif + + /** + * Main query routine of this algorithm. + * Returns an approximation of the minimum cut of the graph defined by the graph stream + * seen thus far. This approximation is guaranteed to be within 1 +/- epsilon of the true + * minimum cut. + */ + MinCut calc_minimum_cut(); + + /** + * Return if we have cached an answer to query. + * This allows the driver to avoid flushing the gutters before calling query functions. + * TODO: Is there something intelligent we can do here for mincut/k-conn + */ + bool has_cached_query(int query_type) { + if (query_type != MINIMUMCUT) return cc_sketches[0]->has_cached_query(query_type); + return false; + } + + /** + * Print the configuration of minimum cut graph sketching algorithm. + */ + void print_configuration() { + std::cout << config; + std::cout << "MCAlg using the following CCAlg config:" << std::endl; + std::cout << cc_config << std::endl; + } + + node_id_t get_num_vertices() { return num_vertices; } + + // time hooks for experiments + std::chrono::duration total_mc_duration; + std::chrono::duration sf_total_duration; + std::chrono::duration viecut_duration; +}; diff --git a/include/return_types.h b/include/return_types.h index b329bfe6..9bd166a8 100644 --- a/include/return_types.h +++ b/include/return_types.h @@ -28,10 +28,19 @@ class ConnectedComponents { // This class defines a spanning forest of a graph class SpanningForest { private: - std::vector edges; node_id_t num_vertices; + std::vector edges; + std::vector sorted_adjacency; + bool has_adjacency = false; public: SpanningForest(node_id_t num_vertices, const std::unordered_set *spanning_forest); const std::vector& get_edges() const { return edges; } + const std::vector& get_sorted_adjacency(); +}; + +struct MinCut { + std::set left_vertices; + std::set right_vertices; + size_t value; }; diff --git a/include/sketch.h b/include/sketch.h index 80473ecc..97a208a0 100644 --- a/include/sketch.h +++ b/include/sketch.h @@ -1,204 +1,9 @@ #pragma once -#include -#include -#include +#include "dense_sketch.h" +#include "sparse_sketch.h" -#include -#include -#include -#include - -#include "util.h" -#include "bucket.h" - -// enum SerialType { -// FULL, -// RANGE, -// SPARSE, -// }; - -enum SampleResult { - GOOD, // sampling this sketch returned a single non-zero value - ZERO, // sampling this sketch returned that there are no non-zero values - FAIL // sampling this sketch failed to produce a single non-zero value -}; - -struct SketchSample { - vec_t idx; - SampleResult result; -}; - -struct ExhaustiveSketchSample { - std::unordered_set idxs; - SampleResult result; -}; - -/** - * Sketch for graph processing, either CubeSketch or CameoSketch. - * Sub-linear representation of a vector. - */ -class Sketch { - private: - const uint64_t seed; // seed for hash functions - size_t num_samples; // number of samples we can perform - size_t cols_per_sample; // number of columns to use on each sample - size_t num_columns; // Total number of columns. (product of above 2) - size_t bkt_per_col; // number of buckets per column - size_t num_buckets; // number of total buckets (product of above 2) - - size_t sample_idx = 0; // number of samples performed so far - - // bucket data - Bucket* buckets; - - public: - /** - * The below constructors use vector length as their input. However, in graph sketching our input - * is the number of vertices. This function converts from number of graph vertices to vector - * length. - * @param num_vertices Number of graph vertices - * @return The length of the vector to sketch - */ - static vec_t calc_vector_length(node_id_t num_vertices) { - return ceil(double(num_vertices) * (num_vertices - 1) / 2); - } - - /** - * This function computes the number of samples a Sketch should support in order to solve - * connected components. Optionally, can increase or decrease the number of samples by a - * multiplicative factor. - * @param num_vertices Number of graph vertices - * @param f Multiplicative sample factor - * @return The number of samples - */ - static size_t calc_cc_samples(node_id_t num_vertices, double f) { - return std::max(size_t(18), (size_t) ceil(f * log2(num_vertices) / num_samples_div)); - } - - /** - * Construct a sketch object - * @param vector_len Length of the vector we are sketching - * @param seed Random seed of the sketch - * @param num_samples [Optional] Number of samples this sketch supports (default = 1) - * @param cols_per_sample [Optional] Number of sketch columns for each sample (default = 1) - */ - Sketch(vec_t vector_len, uint64_t seed, size_t num_samples = 1, - size_t cols_per_sample = default_cols_per_sample); - - /** - * Construct a sketch from a serialized stream - * @param vector_len Length of the vector we are sketching - * @param seed Random seed of the sketch - * @param binary_in Stream holding serialized sketch object - * @param num_samples [Optional] Number of samples this sketch supports (default = 1) - * @param cols_per_sample [Optional] Number of sketch columns for each sample (default = 1) - */ - Sketch(vec_t vector_len, uint64_t seed, std::istream& binary_in, size_t num_samples = 1, - size_t cols_per_sample = default_cols_per_sample); - - /** - * Sketch copy constructor - * @param s The sketch to copy. - */ - Sketch(const Sketch& s); - - ~Sketch(); - - /** - * Update a sketch based on information about one of its indices. - * @param update the point update. - */ - void update(const vec_t update); - - /** - * Function to sample from the sketch. - * cols_per_sample determines the number of columns we allocate to this query - * @return A pair with the result index and a code indicating the type of result. - */ - SketchSample sample(); - - /** - * Function to sample from the appropriate columns to return 1 or more non-zero indices - * @return A pair with the result indices and a code indicating the type of result. - */ - ExhaustiveSketchSample exhaustive_sample(); - - std::mutex mutex; // lock the sketch for applying updates in multithreaded processing - - /** - * In-place merge function. - * @param other Sketch to merge into caller - */ - void merge(const Sketch &other); - - /** - * In-place range merge function. Updates the caller Sketch. - * The range merge only merges some of the Sketches - * This function should only be used if you know what you're doing - * @param other Sketch to merge into caller - * @param start_sample Index of first sample to merge - * @param n_samples Number of samples to merge - */ - void range_merge(const Sketch &other, size_t start_sample, size_t n_samples); - - /** - * Perform an in-place merge function without another Sketch and instead - * use a raw bucket memory. - * We also allow for only a portion of the buckets to be merge at once - * @param raw_bucket Raw bucket data to merge into this sketch - */ - void merge_raw_bucket_buffer(const Bucket *raw_buckets); - - /** - * Zero out all the buckets of a sketch. - */ - void zero_contents(); - - friend bool operator==(const Sketch& sketch1, const Sketch& sketch2); - friend std::ostream& operator<<(std::ostream& os, const Sketch& sketch); - - /** - * Serialize the sketch to a binary output stream. - * @param binary_out the stream to write to. - */ - void serialize(std::ostream& binary_out) const; - - inline void reset_sample_state() { - sample_idx = 0; - } - - // return the size of the sketching datastructure in bytes (just the buckets, not the metadata) - inline size_t bucket_array_bytes() const { return num_buckets * sizeof(Bucket); } - - inline const Bucket* get_readonly_bucket_ptr() const { return (const Bucket*) buckets; } - inline uint64_t get_seed() const { return seed; } - inline size_t column_seed(size_t column_idx) const { return seed + column_idx * 5; } - inline size_t checksum_seed() const { return seed; } - inline size_t get_columns() const { return num_columns; } - inline size_t get_buckets() const { return num_buckets; } - inline size_t get_num_samples() const { return num_samples; } - - static size_t calc_bkt_per_col(size_t n) { return ceil(log2(n)) + 1; } - -#ifdef L0_SAMPLING - static constexpr size_t default_cols_per_sample = 7; - // NOTE: can improve this but leaving for comparison purposes - static constexpr double num_samples_div = log2(3) - 1; +#ifdef L0_FULLY_DENSE +typedef DenseSketch Sketch; #else - static constexpr size_t default_cols_per_sample = 1; - static constexpr double num_samples_div = 1 - log2(2 - 0.8); +typedef SparseSketch Sketch; #endif -}; - -class OutOfSamplesException : public std::exception { - private: - std::string err_msg; - public: - OutOfSamplesException(size_t seed, size_t num_samples, size_t sample_idx) - : err_msg("This sketch (seed=" + std::to_string(seed) + - ", max samples=" + std::to_string(num_samples) + - ") cannot be sampled more times (cur idx=" + std::to_string(sample_idx) + ")!") {} - virtual const char* what() const throw() { - return err_msg.c_str(); - } -}; diff --git a/include/sketch_types.h b/include/sketch_types.h new file mode 100644 index 00000000..19f67b9f --- /dev/null +++ b/include/sketch_types.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include + +#include "types.h" +// enum SerialType { +// FULL, +// RANGE, +// SPARSE, +// }; + +enum SampleResult { + GOOD, // sampling this sketch returned a single non-zero value + ZERO, // sampling this sketch returned that there are no non-zero values + FAIL // sampling this sketch failed to produce a single non-zero value +}; + +struct SketchSample { + vec_t idx; + SampleResult result; +}; + +struct ExhaustiveSketchSample { + std::vector idxs; + SampleResult result; +}; + +class OutOfSamplesException : public std::exception { + private: + std::string err_msg; + + public: + OutOfSamplesException(size_t seed, size_t num_samples, size_t sample_idx) + : err_msg("This sketch (seed=" + std::to_string(seed) + + ", max samples=" + std::to_string(num_samples) + + ") cannot be sampled more times (cur idx=" + std::to_string(sample_idx) + ")!") {} + virtual const char* what() const throw() { return err_msg.c_str(); } +}; diff --git a/include/sparse_sketch.h b/include/sparse_sketch.h new file mode 100644 index 00000000..612234f1 --- /dev/null +++ b/include/sparse_sketch.h @@ -0,0 +1,323 @@ +#pragma once +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "util.h" +#include "bucket.h" +#include "sketch_types.h" + +class DenseSketch; + +#pragma pack(push,1) +struct SparseBucket { + uint16_t next; // index of next sparse bucket in this column + uint8_t row; // row of sparse bucket + Bucket bkt; // actual bucket content +}; +#pragma pack(pop) + +// TODO: Do we want to use row major or column major order? +// So the advantage of row-major is that we can update faster. Most updates will only touch +// first few rows of data-structure. However, could slow down queries. (Although most query +// answers will probably be in sparse data-structure). OH! Also, range_merge is important here +// if column-major then the column we are merging is contig, if not, then not. +// A: Keep column-major for the moment, performance evaluation later. + +/* Memory Allocation of a SparseSketch. Contiguous (only roughly to scale). + Where z is number of non-zero elements in vector we are sketching. + _________________________________________________________________________________________________ +| Dense | Sparse | Linked List | +| Bucket | Bucket | Metadata | +| Region | Region | for Sparse bkts | +| log n * log z buckets | clog n buckets | clogn/16 buckets | +|_________________________________________________|____________________________|__________________| +*/ + +/** + * SparseSketch for graph processing + * Sub-linear representation of a vector. + */ +class SparseSketch { + private: + const uint64_t seed; // seed for hash functions + const size_t num_samples; // number of samples we can perform + const size_t cols_per_sample; // number of columns to use on each sample + const size_t num_columns; // Total number of columns. (product of above 2) + const size_t bkt_per_col; // maximum number of buckets per column (max number of rows) + + size_t num_buckets; // number of total buckets (col * dense_rows + sparse_capacity) + size_t sample_idx = 0; // number of samples performed so far + + // Allocated buckets + Bucket* buckets; + + // number of dense buckets to sample from when sparse region is populated + static constexpr size_t num_dense_to_sample = 2; + + // minimum number of dense rows in a sketch no matter how sparse the vector is. + static constexpr size_t min_num_dense_rows = 6; + size_t num_dense_rows = min_num_dense_rows; + + // Variables for sparse representation of lower levels of bucket Matrix + // TODO: evaluate implications of this constant + static constexpr double sparse_bucket_constant = 3; // constant factor c (see diagram) + SparseBucket* sparse_buckets; // a pointer into the buckets array + uint16_t *ll_metadata; // pointer to heads of column LLs + size_t number_of_sparse_buckets = 0; // cur number of sparse buckets + size_t sparse_capacity = sparse_bucket_constant * num_columns; // max number of sparse buckets + static constexpr size_t max_columns = uint16_t(-1) / sparse_bucket_constant - 1; + + /** + * Reallocates the bucket array if necessary to either grow or shrink the dense region + */ + void reallocate_if_needed(int delta); + void dense_realloc(size_t new_num_dense_rows); + + // These variables let us know how many Buckets to allocate to make space for the SparseBuckets + // and the LL metadata that will use that space + size_t sparse_data_size = ceil(double(sparse_capacity) * sizeof(SparseBucket) / sizeof(Bucket)); + size_t ll_metadata_size = ceil((double(num_columns) + 1) * sizeof(uint16_t) / sizeof(Bucket)); + + void update_sparse(uint16_t col, const SparseBucket &to_add); + SketchSample sample_sparse(size_t first_col, size_t end_col); + + inline uint16_t remove_ll_head(size_t col) { + uint16_t temp = ll_metadata[col]; + ll_metadata[col] = sparse_buckets[ll_metadata[col]].next; + return temp; + } + inline uint16_t claim_free_bucket() { + assert(ll_metadata[num_columns] != uint16_t(-1)); + return remove_ll_head(num_columns); + } + inline void insert_to_ll_head(size_t col, uint16_t add_idx) { + sparse_buckets[add_idx].next = ll_metadata[col]; + ll_metadata[col] = add_idx; + } + inline void free_bucket(uint16_t bkt_idx) { + sparse_buckets[bkt_idx].row = 0; + sparse_buckets[bkt_idx].bkt = {0, 0}; + insert_to_ll_head(num_columns, bkt_idx); + } + inline void insert_to_ll(uint16_t add_idx, SparseBucket &prev) { + sparse_buckets[add_idx].next = prev.next; + prev.next = add_idx; + } + inline void remove_from_ll(SparseBucket& bkt_to_remove, SparseBucket &prev) { + prev.next = bkt_to_remove.next; + } + inline bool merge_sparse_bkt(uint16_t our_idx, const SparseBucket& oth, uint16_t prev_idx, + size_t col) { + SparseBucket &ours = sparse_buckets[our_idx]; + ours.bkt.alpha ^= oth.bkt.alpha; + ours.bkt.gamma ^= oth.bkt.gamma; + if (SketchBucket::is_empty(ours.bkt)) { + if (prev_idx == uint16_t(-1)) + remove_ll_head(col); + else + remove_from_ll(ours, sparse_buckets[prev_idx]); + + free_bucket(our_idx); + return true; + } + return false; + } + + inline Bucket& deterministic_bucket() { + return buckets[0]; + } + inline const Bucket& deterministic_bucket() const { + return buckets[0]; + } + + inline size_t position_func(size_t col, size_t row, size_t num_rows) const { + return col * num_rows + row + 1; // column-major + } + + // return the bucket at a particular index in bucket array + inline Bucket& bucket(size_t col, size_t row) { + assert(row < num_dense_rows); + return buckets[position_func(col, row, num_dense_rows)]; + } + inline const Bucket& bucket(size_t col, size_t row) const { + assert(row < num_dense_rows); + return buckets[position_func(col, row, num_dense_rows)]; + } + + size_t calc_num_buckets(size_t new_num_dense_rows) { + return num_columns * new_num_dense_rows + sparse_data_size + ll_metadata_size + 1; + } + + size_t calc_sparse_index(size_t rows) { + return num_columns * rows + 1; + } + + size_t calc_metadata_index(size_t rows) { + return num_columns * rows + sparse_data_size + 1; + } + + void upd_sparse_ptrs() { + sparse_buckets = (SparseBucket *) &buckets[calc_sparse_index(num_dense_rows)]; + ll_metadata = (uint16_t *) &buckets[calc_metadata_index(num_dense_rows)]; + } + + // given another SparseSketch column, merge it into ours + void merge_sparse_column(const SparseBucket* oth_sparse_buckets, const uint16_t* oth_ll_metadata, + size_t col); + public: + /** + * The below constructors use vector length as their input. However, in graph sketching our input + * is the number of vertices. This function converts from number of graph vertices to vector + * length. + * @param num_vertices Number of graph vertices + * @return The length of the vector to sketch + */ + static vec_t calc_vector_length(node_id_t num_vertices) { + return ceil(double(num_vertices) * (num_vertices - 1) / 2); + } + + /** + * This function computes the number of samples a Sketch should support in order to solve + * connected components. Optionally, can increase or decrease the number of samples by a + * multiplicative factor. + * @param num_vertices Number of graph vertices + * @param f Multiplicative sample factor + * @return The number of samples + */ + static size_t calc_cc_samples(node_id_t num_vertices, double f) { + return std::max(size_t(18), (size_t) ceil(f * log2(num_vertices) / num_samples_div)); + } + + /** + * Construct a sketch object + * @param vector_len Length of the vector we are sketching + * @param seed Random seed of the sketch + * @param num_samples [Optional] Number of samples this sketch supports (default = 1) + * @param cols_per_sample [Optional] Number of sketch columns for each sample (default = 1) + */ + SparseSketch(vec_t vector_len, uint64_t seed, size_t num_samples = 1, + size_t cols_per_sample = default_cols_per_sample); + + /** + * Construct a sketch from a serialized stream + * @param vector_len Length of the vector we are sketching + * @param seed Random seed of the sketch + * @param binary_in Stream holding serialized sketch object + * @param num_buckets Number of buckets in serialized sketch (dense + sparse_capacity) + * @param num_samples [Optional] Number of samples this sketch supports (default = 1) + * @param cols_per_sample [Optional] Number of sketch columns for each sample (default = 1) + */ + SparseSketch(vec_t vector_len, uint64_t seed, std::istream& binary_in, size_t num_buckets, + size_t num_samples = 1, size_t cols_per_sample = default_cols_per_sample); + + /** + * SparseSketch copy constructor + * @param s The sketch to copy. + */ + SparseSketch(const SparseSketch& s); + + ~SparseSketch(); + + /** + * Update a sketch based on information about one of its indices. + * @param update the point update. + */ + void update(const vec_t update); + + /** + * Function to sample from the sketch. + * cols_per_sample determines the number of columns we allocate to this query + * @return A pair with the result index and a code indicating the type of result. + */ + SketchSample sample(); + + /** + * Function to sample from the appropriate columns to return 1 or more non-zero indices + * @return A pair with the result indices and a code indicating the type of result. + */ + ExhaustiveSketchSample exhaustive_sample(); + + std::mutex mutex; // lock the sketch for applying updates in multithreaded processing + + /** + * In-place merge function. + * @param other Sketch to merge into caller + */ + void merge(const SparseSketch &other); + + /** + * In-place range merge function. Updates the caller Sketch. + * The range merge only merges some of the Sketches + * This function should only be used if you know what you're doing + * @param other Sketch to merge into caller + * @param start_sample Index of first sample to merge + * @param n_samples Number of samples to merge + */ + void range_merge(const SparseSketch &other, size_t start_sample, size_t n_samples); + + /** + * Perform an in-place merge function without another Sketch and instead + * use a raw bucket memory. + * We also allow for only a portion of the buckets to be merge at once + * @param raw_bucket Raw bucket data to merge into this sketch + * @param n_raw_buckets Size of raw_buckets in number of Bucket data-structures + */ + void merge_raw_bucket_buffer(const Bucket *raw_buckets, size_t n_raw_buckets); + + /** + * Zero out all the buckets of a sketch. + */ + void zero_contents(); + + friend bool operator==(const SparseSketch& sketch1, const SparseSketch& sketch2); + friend bool operator==(const SparseSketch& sparse, const DenseSketch& dense); + friend std::ostream& operator<<(std::ostream& os, const SparseSketch& sketch); + + /** + * Serialize the sketch to a binary output stream. + * @param binary_out the stream to write to. + */ + void serialize(std::ostream& binary_out) const; + + inline void reset_sample_state() { + sample_idx = 0; + } + + // return the size of the sketching datastructure in bytes (just the buckets, not the metadata) + inline size_t bucket_array_bytes() const { + return num_buckets * sizeof(Bucket); + } + + // return the size of a sketch given vector size n and number of samples s + static size_t estimate_bytes(size_t /*n*/, size_t s) { + size_t num_cols = s * default_cols_per_sample; + size_t metadata_size = ceil(double(num_cols + 1) * sizeof(uint16_t) / sizeof(Bucket)) * sizeof(Bucket); + size_t sparse_size = + ceil(double(num_cols) * sparse_bucket_constant * sizeof(SparseBucket) / sizeof(Bucket)) * + sizeof(Bucket); + size_t dense_size = (1 + num_cols * min_num_dense_rows) * sizeof(Bucket); + + return dense_size + sparse_size + metadata_size; + } + + inline const Bucket* get_readonly_bucket_ptr() const { return (const Bucket*) buckets; } + inline uint64_t get_seed() const { return seed; } + inline size_t column_seed(size_t column_idx) const { return seed + column_idx * 5; } + inline size_t checksum_seed() const { return seed; } + inline size_t get_columns() const { return num_columns; } + inline size_t get_buckets() const { return num_buckets; } + inline size_t get_num_samples() const { return num_samples; } + inline size_t get_num_dense_rows() const { return num_dense_rows; } + + static size_t calc_bkt_per_col(size_t n) { return ceil(log2(n)) + 1; } + + static constexpr size_t default_cols_per_sample = 1; + static constexpr double num_samples_div = 1 - log2(2 - 0.8); +}; diff --git a/include/types.h b/include/types.h index 6fea6b26..bed22941 100644 --- a/include/types.h +++ b/include/types.h @@ -1,10 +1,12 @@ #pragma once -#include +#include #include +#include + #include -#include +#include -typedef uint64_t col_hash_t; +typedef uint32_t col_hash_t; static const auto& vec_hash = XXH3_64bits_withSeed; static const auto& col_hash = XXH3_64bits_withSeed; @@ -13,3 +15,26 @@ struct GraphUpdate { Edge edge; UpdateType type; }; + +struct SubgraphTaggedUpdate { + node_id_t subgraph; // highest index subgraph the edge maps to (same src, dst -> same subgraph) + node_id_t dst; // destination vertex of edge + + bool operator<(const SubgraphTaggedUpdate& oth) const { + if (subgraph == oth.subgraph) return dst < oth.dst; + + return subgraph < oth.subgraph; + } + + bool operator>(const SubgraphTaggedUpdate& oth) const { + if (subgraph == oth.subgraph) return dst > oth.dst; + + return subgraph > oth.subgraph; + } +}; + +struct TaggedUpdateBatch { + node_id_t src; + node_id_t edge_store_subgraph; + std::vector dsts_data; +}; diff --git a/src/cc_alg_configuration.cpp b/src/cc_alg_configuration.cpp index becbb7e5..2ce12af1 100644 --- a/src/cc_alg_configuration.cpp +++ b/src/cc_alg_configuration.cpp @@ -34,6 +34,11 @@ std::ostream& operator<< (std::ostream &out, const CCAlgConfiguration &conf) { #else out << " Sketching algorithm = CameoSketch" << std::endl; #endif +#ifdef L0_FULLY_DENSE + out << " Sketch storage = Dense Matrix" << std::endl; +#else + out << " Sketch storage = Hybrid Matrix" << std::endl; +#endif #ifdef NO_EAGER_DSU out << " Using Eager DSU = False" << std::endl; #else diff --git a/src/cc_sketch_alg.cpp b/src/cc_sketch_alg.cpp index a1e688db..8c399067 100644 --- a/src/cc_sketch_alg.cpp +++ b/src/cc_sketch_alg.cpp @@ -10,14 +10,12 @@ CCSketchAlg::CCSketchAlg(node_id_t num_vertices, size_t seed, CCAlgConfiguration config) : num_vertices(num_vertices), seed(seed), dsu(num_vertices), config(config) { - representatives = new std::set(); sketches = new Sketch *[num_vertices]; vec_t sketch_vec_len = Sketch::calc_vector_length(num_vertices); size_t sketch_num_samples = Sketch::calc_cc_samples(num_vertices, config.get_sketches_factor()); for (node_id_t i = 0; i < num_vertices; ++i) { - representatives->insert(i); sketches[i] = new Sketch(sketch_vec_len, seed, sketch_num_samples); } @@ -25,6 +23,8 @@ CCSketchAlg::CCSketchAlg(node_id_t num_vertices, size_t seed, CCAlgConfiguration spanning_forest_mtx = new std::mutex[num_vertices]; dsu_valid = true; shared_dsu_valid = true; + + num_updates = 0; } CCSketchAlg *CCSketchAlg::construct_from_serialized_data(const std::string &input_file, @@ -45,15 +45,16 @@ CCSketchAlg *CCSketchAlg::construct_from_serialized_data(const std::string &inpu CCSketchAlg::CCSketchAlg(node_id_t num_vertices, size_t seed, std::ifstream &binary_stream, CCAlgConfiguration config) : num_vertices(num_vertices), seed(seed), dsu(num_vertices), config(config) { - representatives = new std::set(); sketches = new Sketch *[num_vertices]; vec_t sketch_vec_len = Sketch::calc_vector_length(num_vertices); size_t sketch_num_samples = Sketch::calc_cc_samples(num_vertices, config.get_sketches_factor()); for (node_id_t i = 0; i < num_vertices; ++i) { - representatives->insert(i); - sketches[i] = new Sketch(sketch_vec_len, seed, binary_stream, sketch_num_samples); + size_t num_bkts_in_sketch; + binary_stream.read((char *) &num_bkts_in_sketch, sizeof(num_bkts_in_sketch)); + sketches[i] = + new Sketch(sketch_vec_len, seed, binary_stream, num_bkts_in_sketch, sketch_num_samples); } binary_stream.close(); @@ -71,7 +72,6 @@ CCSketchAlg::~CCSketchAlg() { delete[] delta_sketches; } - delete representatives; delete[] spanning_forest; delete[] spanning_forest_mtx; } @@ -109,6 +109,9 @@ void CCSketchAlg::apply_update_batch(int thr_id, node_id_t src_vertex, Sketch &delta_sketch = *delta_sketches[thr_id]; delta_sketch.zero_contents(); + // TODO: Remove this later + num_updates += dst_vertices.size(); + for (const auto &dst : dst_vertices) { delta_sketch.update(static_cast(concat_pairing_fn(src_vertex, dst))); } @@ -117,9 +120,10 @@ void CCSketchAlg::apply_update_batch(int thr_id, node_id_t src_vertex, sketches[src_vertex]->merge(delta_sketch); } -void CCSketchAlg::apply_raw_buckets_update(node_id_t src_vertex, Bucket *raw_buckets) { +void CCSketchAlg::apply_raw_buckets_update(node_id_t src_vertex, Bucket *raw_buckets, + size_t num_buckets) { std::lock_guard lk(sketches[src_vertex]->mutex); - sketches[src_vertex]->merge_raw_bucket_buffer(raw_buckets); + sketches[src_vertex]->merge_raw_bucket_buffer(raw_buckets, num_buckets); } // Note: for performance reasons route updates through the driver instead of calling this function @@ -136,12 +140,13 @@ void CCSketchAlg::update(GraphUpdate upd) { // that is, 1 or more vertices merged together during Boruvka inline bool CCSketchAlg::sample_supernode(Sketch &skt) { bool modified = false; + SketchSample sample = skt.sample(); Edge e = inv_concat_pairing_fn(sample.idx); SampleResult result_type = sample.result; - // std::cout << " " << result_type << " e:" << e.src << " " << e.dst << std::endl; + // std::cerr << " " << result_type << " e:" << e.src << " " << e.dst << std::endl; if (result_type == FAIL) { modified = true; @@ -221,6 +226,8 @@ inline bool merge_global(const size_t cur_round, const Sketch &local_sketch, // faster query procedure optimized for when we know there is no merging to do (i.e. round 0) inline bool CCSketchAlg::run_round_zero() { + // std::cout << "Running round zero! " << "num_vertices = " << num_vertices << std::endl; + bool modified = false; bool except = false; std::exception_ptr err; @@ -488,7 +495,7 @@ void CCSketchAlg::boruvka_emulation() { // << std::endl; while (true) { - // std::cout << " Round: " << round_num << std::endl; + // std::cerr << " Round: " << round_num << std::endl; // start = std::chrono::steady_clock::now(); modified = perform_boruvka_round(round_num, merge_instr, global_merges); // std::cout << " perform_boruvka_round = " @@ -512,9 +519,7 @@ void CCSketchAlg::boruvka_emulation() { update_locked = false; } -ConnectedComponents CCSketchAlg::connected_components() { - cc_alg_start = std::chrono::steady_clock::now(); - +void CCSketchAlg::compute_dsu() { // if the DSU holds the answer, use that if (shared_dsu_valid) { #ifdef VERIFY_SAMPLES_F @@ -524,30 +529,34 @@ ConnectedComponents CCSketchAlg::connected_components() { } } #endif + return; } // The DSU does not hold the answer, make it so - else { - bool except = false; - std::exception_ptr err; - try { - // auto start = std::chrono::steady_clock::now(); - boruvka_emulation(); - // std::cout << " boruvka's algorithm = " - // << std::chrono::duration(std::chrono::steady_clock::now() - start).count() - // << std::endl; - } catch (...) { - except = true; - err = std::current_exception(); - } - - // get ready for ingesting more from the stream by resetting the sketches sample state - for (node_id_t i = 0; i < num_vertices; i++) { - sketches[i]->reset_sample_state(); - } + bool except = false; + std::exception_ptr err; + try { + // auto start = std::chrono::steady_clock::now(); + boruvka_emulation(); + // std::cout << " boruvka's algorithm = " + // << std::chrono::duration(std::chrono::steady_clock::now() - start).count() + // << std::endl; + } catch (...) { + except = true; + err = std::current_exception(); + } - if (except) std::rethrow_exception(err); + // get ready for ingesting more from the stream by resetting the sketches sample state + for (node_id_t i = 0; i < num_vertices; i++) { + sketches[i]->reset_sample_state(); } + if (except) std::rethrow_exception(err); +} + +ConnectedComponents CCSketchAlg::connected_components() { + cc_alg_start = std::chrono::steady_clock::now(); + + compute_dsu(); ConnectedComponents cc(num_vertices, dsu); #ifdef VERIFY_SAMPLES_F verifier->verify_connected_components(cc); @@ -557,8 +566,7 @@ ConnectedComponents CCSketchAlg::connected_components() { } SpanningForest CCSketchAlg::calc_spanning_forest() { - // TODO: Could probably optimize this a bit by writing new code - connected_components(); + compute_dsu(); SpanningForest ret(num_vertices, spanning_forest); #ifdef VERIFY_SAMPLES_F @@ -567,6 +575,97 @@ SpanningForest CCSketchAlg::calc_spanning_forest() { return ret; } +void CCSketchAlg::filter_sf_edges(SpanningForest &sf) { + // auto start = std::chrono::steady_clock::now(); + + dsu_valid = false; + shared_dsu_valid = false; + + const std::vector &edges = sf.get_sorted_adjacency(); + +#pragma omp parallel + { + size_t thr_id = omp_get_thread_num(); + size_t num_threads = omp_get_num_threads(); + + std::pair partition = get_ith_partition(edges.size(), thr_id, num_threads); + size_t start = partition.first; + size_t end = partition.second; + + if (start != end) { + // check if we collide with previous thread. If so lock and apply those updates. + if (start > 0 && edges[start].src == edges[start - 1].src) { + sketches[edges[start].src]->mutex.lock(); + size_t orig_start = start; + while (start < end && edges[start].src == edges[orig_start].src) { + Edge edge = edges[start]; + sketches[edge.src]->update(static_cast(concat_pairing_fn(edge.src, edge.dst))); + ++start; + } + + sketches[edges[orig_start].src]->mutex.unlock(); + } + + // check if we collide with next thread. If so lock and apply those updates. + if (end > start && end < edges.size() && edges[end - 1].src == edges[end].src) { + sketches[edges[end - 1].src]->mutex.lock(); + size_t orig_end = end; + while (end > start && edges[end - 1].src == edges[orig_end - 1].src) { + Edge edge = edges[end - 1]; + sketches[edge.src]->update(static_cast(concat_pairing_fn(edge.src, edge.dst))); + --end; + } + + sketches[edges[orig_end].src]->mutex.unlock(); + } + + for (size_t i = start; i < end; i++) { + Edge edge = edges[i]; + sketches[edge.src]->update(static_cast(concat_pairing_fn(edge.src, edge.dst))); + } + } + } + + // delete_time += std::chrono::steady_clock::now() - start; +} + +std::vector CCSketchAlg::calc_disjoint_spanning_forests(size_t k) { + std::cout << "Spanning forests query. Number of updates = " << num_updates.load() << std::endl; + + // sf_query_start = std::chrono::steady_clock::now(); + std::vector SFs; + size_t max_rounds = 0; + + for (size_t i = 0; i < k; i++) { + std::cout << " Spanning forest: " << i << std::endl; + compute_dsu(); + + SFs.emplace_back(num_vertices, spanning_forest); + max_rounds = std::max(last_query_rounds, max_rounds); + + filter_sf_edges(SFs[SFs.size() - 1]); + + std::cout << "Spanning Forest " << i << " size = " << SFs[SFs.size() - 1].get_edges().size() << std::endl; + std::cout << "Last query rounds = " << last_query_rounds << std::endl; + if (SFs[SFs.size() - 1].get_edges().size() == 0) break; + } + + // revert the state of the sketches to remove all deletions + for (auto &sf : SFs) { + filter_sf_edges(sf); + } + +#ifdef VERIFY_SAMPLES_F + verifier->verify_spanning_forests(SFs); +#endif + + // number of rounds per SF may be non-monotonic, so we track the maximum number of rounds + // among all of the spanning forest queries. + last_query_rounds = max_rounds; + + return SFs; +} + bool CCSketchAlg::point_query(node_id_t a, node_id_t b) { cc_alg_start = std::chrono::steady_clock::now(); @@ -617,6 +716,8 @@ void CCSketchAlg::write_binary(const std::string &filename) { binary_out.write((char *)&num_vertices, sizeof(num_vertices)); binary_out.write((char *)&config._sketches_factor, sizeof(config._sketches_factor)); for (node_id_t i = 0; i < num_vertices; ++i) { + size_t num_bkts_in_sketch = sketches[i]->get_buckets(); + binary_out.write((char*) &num_bkts_in_sketch, sizeof(num_bkts_in_sketch)); sketches[i]->serialize(binary_out); } binary_out.close(); diff --git a/src/dense_sketch.cpp b/src/dense_sketch.cpp new file mode 100644 index 00000000..2e784b66 --- /dev/null +++ b/src/dense_sketch.cpp @@ -0,0 +1,256 @@ +#include "dense_sketch.h" + +#include +#include +#include +#include +#include + +DenseSketch::DenseSketch(vec_t vector_len, uint64_t seed, size_t _samples, size_t _cols) + : seed(seed), + num_samples(_samples), + cols_per_sample(_cols), + num_columns(cols_per_sample * num_samples), + bkt_per_col(calc_bkt_per_col(vector_len)) { + + num_buckets = num_columns * bkt_per_col + 1; // plus 1, deterministic bucket + buckets = new Bucket[num_buckets]; + + // initialize bucket values + for (size_t i = 0; i < num_buckets; ++i) { + buckets[i].alpha = 0; + buckets[i].gamma = 0; + } +} + +DenseSketch::DenseSketch(vec_t vector_len, uint64_t seed, std::istream &binary_in, + size_t num_buckets, size_t _samples, size_t _cols) + : seed(seed), + num_samples(_samples), + cols_per_sample(_cols), + num_columns(cols_per_sample * num_samples), + bkt_per_col(calc_bkt_per_col(vector_len)), + num_buckets(num_buckets) { + if (num_buckets != num_columns * bkt_per_col + 1) { + throw std::invalid_argument("Serial Constructor: Number of buckets does not match expectation"); + } + num_buckets = num_columns * bkt_per_col + 1; // plus 1 for deterministic bucket + buckets = new Bucket[num_buckets]; + + // Read the serialized Sketch contents + binary_in.read((char *)buckets, bucket_array_bytes()); +} + +DenseSketch::DenseSketch(const DenseSketch &s) + : seed(s.seed), + num_samples(s.num_samples), + cols_per_sample(s.cols_per_sample), + num_columns(s.num_columns), + bkt_per_col(s.bkt_per_col) { + num_buckets = s.num_buckets; + buckets = new Bucket[num_buckets]; + + std::memcpy(buckets, s.buckets, bucket_array_bytes()); +} + +DenseSketch::~DenseSketch() { delete[] buckets; } + + +void DenseSketch::update(const vec_t update_idx) { + vec_hash_t checksum = SketchBucket::get_index_hash(update_idx, checksum_seed()); + + // Update depth 0 bucket + SketchBucket::update(deterministic_bucket(), update_idx, checksum); + + // Update higher depth buckets + SketchBucket::Depths depths; + for (size_t i = 0; i < num_columns - 1; i += 2) { + depths = SketchBucket::get_index_depths(update_idx, column_seed(i), bkt_per_col); + for (size_t j = 0; j < 2; j++) { + col_hash_t depth = depths[j]; + likely_if(depth < bkt_per_col) { + SketchBucket::update(bucket(i + j, depth), update_idx, checksum); + } + } + } + if ((num_columns & 0x1) == 1) { + size_t col = num_columns - 1; + size_t depth = SketchBucket::get_index_depth(update_idx, column_seed(col), bkt_per_col); + likely_if(depth < bkt_per_col) { + SketchBucket::update(bucket(col, depth), update_idx, checksum); + } + } +} + +static void is_empty(DenseSketch &skt) { + const Bucket* buckets = skt.get_readonly_bucket_ptr(); + for (size_t i = 0; i < skt.get_buckets(); i++) { + if (!SketchBucket::is_empty(buckets[i])) { + std::cerr << "FOUND NOT EMPTY BUCKET!" << std::endl; + } + } +} + +// TODO: Switch the L0_SAMPLING flag to instead affect query procedure. +// (Only use deepest bucket. We don't need the alternate update procedure in the code anymore.) + +void DenseSketch::zero_contents() { + for (size_t i = 0; i < num_buckets; i++) { + buckets[i].alpha = 0; + buckets[i].gamma = 0; + } + reset_sample_state(); +} + +SketchSample DenseSketch::sample() { + if (sample_idx >= num_samples) { + throw OutOfSamplesException(seed, num_samples, sample_idx); + } + + size_t idx = sample_idx++; + size_t first_column = idx * cols_per_sample; + + // std::cout << "Sampling: " << first_column << ", " << first_column + cols_per_sample << std::endl; + + // std::cout << *this << std::endl; + + if (SketchBucket::is_empty(deterministic_bucket())) { + is_empty(*this); + return {0, ZERO}; // the "first" bucket is deterministic so if all zero then no edges to return + } + + if (SketchBucket::is_good(deterministic_bucket(), checksum_seed())) + return {deterministic_bucket().alpha, GOOD}; + + for (size_t i = 0; i < cols_per_sample; ++i) { + for (size_t j = 0; j < bkt_per_col; ++j) { + if (SketchBucket::is_good(bucket(i + first_column, j), checksum_seed())) + return {bucket(i + first_column, j).alpha, GOOD}; + } + } + return {0, FAIL}; +} + +ExhaustiveSketchSample DenseSketch::exhaustive_sample() { + if (sample_idx >= num_samples) { + throw OutOfSamplesException(seed, num_samples, sample_idx); + } + std::vector ret; + + size_t idx = sample_idx++; + size_t first_column = idx * cols_per_sample; + + unlikely_if (deterministic_bucket().alpha == 0 && deterministic_bucket().gamma == 0) + return {ret, ZERO}; // the "first" bucket is deterministic so if zero then no edges to return + + unlikely_if (SketchBucket::is_good(deterministic_bucket(), checksum_seed())) { + ret.push_back(deterministic_bucket().alpha); + return {ret, GOOD}; + } + + for (size_t i = 0; i < cols_per_sample; ++i) { + for (size_t j = 0; j < bkt_per_col; ++j) { + unlikely_if (SketchBucket::is_good(bucket(i + first_column, j), checksum_seed())) { + ret.push_back(bucket(i + first_column, j).alpha); + } + } + } + + unlikely_if (ret.size() == 0) + return {ret, FAIL}; + return {ret, GOOD}; +} + +void DenseSketch::merge(const DenseSketch &other) { + for (size_t i = 0; i < num_buckets; ++i) { + buckets[i].alpha ^= other.buckets[i].alpha; + buckets[i].gamma ^= other.buckets[i].gamma; + } +} + +void DenseSketch::range_merge(const DenseSketch &other, size_t start_sample, size_t n_samples) { + if (start_sample + n_samples > num_samples) { + assert(false); + sample_idx = num_samples; // sketch is in a fail state! + return; + } + + // std::cout << "MERGING THIS" << std::endl; + // std::cout << *this << std::endl; + // std::cout << "WITH THIS" << std::endl; + // std::cout << other << std::endl; + + // update sample idx to point at beginning of this range if before it + sample_idx = std::max(sample_idx, start_sample); + + // merge deterministic bucket + // TODO: I don't like this. Repeated calls to range_merge on same sketches will potentially cause us issues + deterministic_bucket().alpha ^= other.deterministic_bucket().alpha; + deterministic_bucket().gamma ^= other.deterministic_bucket().gamma; + + // merge other buckets + size_t start_column = start_sample * cols_per_sample; + size_t end_column = (start_sample + n_samples) * cols_per_sample; + + // std::cout << start_column << ", " << end_column << std::endl; + for (size_t i = start_column; i < end_column; i++) { + for (size_t j = 0; j < bkt_per_col; j++) { + bucket(i, j).alpha ^= other.bucket(i, j).alpha; + bucket(i, j).gamma ^= other.bucket(i, j).gamma; + } + } + + // std::cout << "RESULT" << std::endl; + // std::cout << *this << std::endl; +} + +void DenseSketch::merge_raw_bucket_buffer(const Bucket *raw_buckets, size_t n_raw_buckets) { + if (n_raw_buckets != num_buckets) { + throw std::invalid_argument("Raw bucket buffer is not the same size as DenseSketch"); + } + + for (size_t i = 0; i < num_buckets; i++) { + buckets[i].alpha ^= raw_buckets[i].alpha; + buckets[i].gamma ^= raw_buckets[i].gamma; + } +} + +void DenseSketch::serialize(std::ostream &binary_out) const { + binary_out.write((char*) buckets, bucket_array_bytes()); +} + +bool operator==(const DenseSketch &sketch1, const DenseSketch &sketch2) { + if (sketch1.num_buckets != sketch2.num_buckets || sketch1.seed != sketch2.seed) + return false; + + for (size_t i = 0; i < sketch1.num_buckets; ++i) { + if (sketch1.buckets[i].alpha != sketch2.buckets[i].alpha || + sketch1.buckets[i].gamma != sketch2.buckets[i].gamma) { + return false; + } + } + + return true; +} + +std::ostream &operator<<(std::ostream &os, const DenseSketch &sketch) { + Bucket bkt = sketch.deterministic_bucket(); + bool good = SketchBucket::is_good(bkt, sketch.checksum_seed()); + vec_t a = bkt.alpha; + vec_hash_t c = bkt.gamma; + + os << " a:" << a << " c:" << c << (good ? " good" : " bad") << std::endl; + + for (unsigned i = 0; i < sketch.num_columns; ++i) { + for (unsigned j = 0; j < sketch.bkt_per_col; ++j) { + Bucket bkt = sketch.bucket(i, j); + vec_t a = bkt.alpha; + vec_hash_t c = bkt.gamma; + bool good = SketchBucket::is_good(bkt, sketch.checksum_seed()); + + os << " a:" << a << " c:" << c << (good ? " good" : " bad") << std::endl; + } + os << std::endl; + } + return os; +} diff --git a/src/driver_configuration.cpp b/src/driver_configuration.cpp index 563bb842..066afd41 100644 --- a/src/driver_configuration.cpp +++ b/src/driver_configuration.cpp @@ -12,12 +12,22 @@ DriverConfiguration& DriverConfiguration::disk_dir(std::string disk_dir) { return *this; } -DriverConfiguration& DriverConfiguration::worker_threads(size_t num_worker_threads) { - _num_worker_threads = num_worker_threads; - if (_num_worker_threads < 1) { - std::cout << "num_worker_threads="<< _num_worker_threads << " is out of bounds. [1, infty)" +DriverConfiguration& DriverConfiguration::worker_threads(size_t num_threads) { + if (num_threads < 1) { + std::cout << "num_worker_threads = "<< num_threads << " is out of bounds. [1, infty)" << "Defaulting to 1." << std::endl; - _num_worker_threads = 1; + } else { + _num_worker_threads = num_threads; + } + return *this; +} + +DriverConfiguration& DriverConfiguration::stream_threads(size_t num_threads) { + if (num_threads < 1) { + std::cout << "num_stream_threads = "<< num_threads << " is out of bounds. [1, infty)" + << "Defaulting to 1." << std::endl; + } else { + _num_stream_threads = num_threads; } return *this; } @@ -35,6 +45,7 @@ std::ostream& operator<< (std::ostream &out, const DriverConfiguration &conf) { gutter_system = "CacheTree"; out << " Guttering system = " << gutter_system << std::endl; out << " Worker thread count = " << conf._num_worker_threads << std::endl; + out << " Stream thread count = " << conf._num_stream_threads << std::endl; out << " On disk data location = " << conf._disk_dir; return out; } diff --git a/src/edge_store.cpp b/src/edge_store.cpp new file mode 100644 index 00000000..62111f44 --- /dev/null +++ b/src/edge_store.cpp @@ -0,0 +1,312 @@ +#include "edge_store.h" +#include "bucket.h" +#include "util.h" + +#include + +// Constructor +EdgeStore::EdgeStore(size_t seed, node_id_t num_vertices, size_t sketch_bytes, size_t num_subgraphs, size_t start_subgraph) + : seed(seed), + num_vertices(num_vertices), + num_subgraphs(num_subgraphs), + adjlist(num_vertices), + vertex_contracted(num_vertices, true), + max_edges(num_vertices * sketch_bytes / store_edge_bytes), + default_buffer_allocation(max_edges / num_vertices) { + num_edges = 0; + adj_mutex = new std::mutex[num_vertices]; + + cur_subgraph = start_subgraph; + true_min_subgraph = start_subgraph; + + // reserve some space for each + for (node_id_t i = 0; i < num_vertices; i++) { + adjlist[i].reserve(default_buffer_allocation); + } +#ifdef VERIFY_SAMPLES_F + num_inserted = 0; + num_duplicate = 0; + num_returned = 0; +#endif +} + +EdgeStore::~EdgeStore() { + delete[] adj_mutex; +#ifdef VERIFY_SAMPLES_F + std::cerr << "EdgeStore: Deconstructor" << std::endl; + std::cerr << " num_edges = " << num_edges << std::endl; + std::cerr << " max_edges = " << max_edges << std::endl; + std::cerr << " num_inserted = " << num_inserted << std::endl; + std::cerr << " num_duplicate = " << num_duplicate << std::endl; + std::cerr << " num_returned = " << num_returned << std::endl; +#endif +} + +// caller_first_es_subgraph is implied to be 0 when calling this function +TaggedUpdateBatch EdgeStore::insert_adj_edges(node_id_t src, + const std::vector &dst_vertices) { + + std::vector tagged_updates; + tagged_updates.resize(dst_vertices.size()); + for (node_id_t i = 0; i < dst_vertices.size(); i++) { + node_id_t dst = dst_vertices[i]; + auto idx = concat_pairing_fn(src, dst); + tagged_updates[i] = {SketchBucket::get_index_depth(idx, seed, num_subgraphs), dst}; + } + return insert_adj_edges(src, 0, tagged_updates); +} + +TaggedUpdateBatch EdgeStore::insert_adj_edges(node_id_t src, node_id_t caller_first_es_subgraph, + std::vector &dst_data) { + std::vector ret; + if (dst_data.size() == 0) return {src, cur_subgraph, ret}; + node_id_t cur_first_es_subgraph; + +#ifdef VERIFY_SAMPLES_F + num_inserted += dst_data.size(); +#endif + + + // Sort the input data + std::sort(dst_data.begin(), dst_data.end()); + + // remove pairs of duplicate updates if there are any + size_t ptr = 0; + size_t dst_ptr = 0; + while (ptr < dst_data.size() - 1) { + if (dst_data[ptr] < dst_data[ptr+1]) { + // not a pair, write to output + dst_data[dst_ptr] = dst_data[ptr]; + ++ptr; + ++dst_ptr; + } else { + // found a pair, skip it + ptr += 2; + } + } + if (ptr < dst_data.size()) { + dst_data[dst_ptr] = dst_data[ptr]; + ++dst_ptr; + } + size_t dst_data_size = dst_ptr; + + // merge the input data into the vertex buffer + { + std::lock_guard lk(adj_mutex[src]); + cur_first_es_subgraph = cur_subgraph; + + if (true_min_subgraph < cur_first_es_subgraph && !vertex_contracted[src]) { + ret = vertex_contract(src); + } + + auto &data_buffer = adjlist[src]; + size_t orig_size = data_buffer.size(); + std::vector new_data_buffer(orig_size + dst_data_size); + + size_t out_ptr = 0; + size_t update_ptr = 0; + size_t buffer_ptr = 0; + + // skip any updates that go to a smaller subgraph + while (update_ptr < dst_data_size && dst_data[update_ptr].subgraph < cur_first_es_subgraph) { + ret.push_back(dst_data[update_ptr]); + ++update_ptr; + } + +#ifdef VERIFY_SAMPLES_F + num_returned += update_ptr; + size_t local_ignored = update_ptr; +#endif + + // merge new updates in, until one of the arrays runs out + while (buffer_ptr < orig_size && update_ptr < dst_data_size) { + if (data_buffer[buffer_ptr] > dst_data[update_ptr]) { + // place update_ptr data into output + new_data_buffer[out_ptr++] = dst_data[update_ptr++]; + } else if (data_buffer[buffer_ptr] < dst_data[update_ptr]) { + // place contents of compare_ptr into out_ptr + new_data_buffer[out_ptr++] = data_buffer[buffer_ptr++]; + } else { + // they are equal! Skip both + ++buffer_ptr; + ++update_ptr; + +#ifdef VERIFY_SAMPLES_F + num_duplicate += 2; + local_ignored += 2; +#endif + } + } + + // place all remaining updates into the buffer + while (buffer_ptr < orig_size) { + new_data_buffer[out_ptr++] = data_buffer[buffer_ptr++]; + } + while (update_ptr < dst_data_size) { + new_data_buffer[out_ptr++] = dst_data[update_ptr++]; + } + new_data_buffer.resize(out_ptr); + std::swap(data_buffer, new_data_buffer); + +#ifdef VERIFY_SAMPLES_F + // verify sorted order + SubgraphTaggedUpdate prev = data_buffer[0]; + for (size_t i = 1; i < data_buffer.size(); i++) { + SubgraphTaggedUpdate data = data_buffer[i]; + if (data < prev || !(data < prev || prev < data)) { + std::cerr << "ERROR: Buffer not sort good!" << std::endl; + std::cerr << "cur " << i << " = {" << data.subgraph << "," << data.dst << "} should be >= "; + std::cerr << "prev = {" << prev.subgraph << "," << prev.dst << "}" << std::endl; + exit(EXIT_FAILURE); + } + } + + if (data_buffer.size() != orig_size + dst_data_size - local_ignored) { + std::cerr << "ERROR: Number of updates incorrect!" << std::endl; + std::cerr << "Expected: " << orig_size + dst_data_size - local_ignored << std::endl; + std::cerr << "Got: " << out_ptr << std::endl; + + std::cerr << "orig_size = " << orig_size << std::endl; + std::cerr << "dst_data_size = " << dst_data_size << std::endl; + std::cerr << "local_ignored = " << local_ignored << std::endl; + exit(EXIT_FAILURE); + } +#endif + + num_edges += data_buffer.size() - orig_size; + } + + if (ret.size() == 0 && true_min_subgraph < cur_first_es_subgraph) { + return vertex_advance_subgraph(cur_first_es_subgraph); + } else { + check_if_too_big(); + return {src, cur_first_es_subgraph, ret}; + } +} + +// IMPORTANT: We must have completed any pending contractions before we call this function +std::vector EdgeStore::get_edges() { + std::vector ret; + ret.reserve(num_edges); + + for (node_id_t src = 0; src < num_vertices; src++) { + for (auto data : adjlist[src]) { + ret.push_back({src, data.dst}); + } + } + + return ret; +} + +#ifdef VERIFY_SAMPLES_F +void EdgeStore::verify_contract_complete() { + for (size_t i = 0; i < num_vertices; i++) { + std::lock_guard lk(adj_mutex[i]); + if (adjlist[i].size() == 0) continue; + + auto it = adjlist[i].begin(); + if (it->subgraph < cur_subgraph) { + std::cerr << "ERROR: Found " << it->subgraph << ", " << it->dst << " which should have been deleted by contraction to " << cur_subgraph << std::endl; + exit(EXIT_FAILURE); + } + } + std::cerr << "Contraction verified!" << std::endl; +} +#endif + +// the thread MUST hold the lock on src before calling this function +std::vector EdgeStore::vertex_contract(node_id_t src) { + std::vector ret; + // someone already contacted this vertex + if (vertex_contracted[src]) + return ret; + + vertex_contracted[src] = true; + auto &data_buffer = adjlist[src]; + size_t orig_size = data_buffer.size(); + + if (data_buffer.size() == 0) { + return ret; + } + + size_t less_than = 0; + while (less_than < data_buffer.size() && data_buffer[less_than].subgraph < cur_subgraph) { + ++less_than; + } + + ret.insert(ret.end(), data_buffer.begin(), data_buffer.begin() + less_than); + + size_t keep_idx = 0; + for (size_t i = less_than; i < data_buffer.size(); i++) { + data_buffer[keep_idx++] = data_buffer[i]; + } + + data_buffer.resize(keep_idx); + + num_edges += data_buffer.size() - orig_size; +#ifdef VERIFY_SAMPLES_F + num_returned += ret.size(); +#endif + return ret; +} + +TaggedUpdateBatch EdgeStore::vertex_advance_subgraph(node_id_t cur_first_es_subgraph) { + node_id_t src = 0; + while (true) { + src = needs_contraction.fetch_add(1); + + if (src >= num_vertices) { + if (src == num_vertices) { + std::lock_guard lk(contract_lock); +#ifdef VERIFY_SAMPLES_F + verify_contract_complete(); +#endif + ++true_min_subgraph; + std::cerr << "EdgeStore: Contraction complete " << std::endl; + } + return {0, cur_first_es_subgraph, std::vector()}; + } + + std::lock_guard lk(adj_mutex[src]); + if (adjlist[src].size() > 0 && !vertex_contracted[src]) + break; + + vertex_contracted[src] = true; + } + + std::lock_guard lk(adj_mutex[src]); + return {src, cur_first_es_subgraph, vertex_contract(src)}; +} + +// checks if we should perform a contraction and begins the process if so +void EdgeStore::check_if_too_big() { + if (num_edges < max_edges) { + // no contraction needed + return; + } + + // we may need to perform a contraction + { + std::lock_guard lk(contract_lock); + if (true_min_subgraph < cur_subgraph) { + // another thread already started contraction + return; + } + + for (node_id_t i = 0; i < num_vertices; i++) { + vertex_contracted[i] = false; + } + needs_contraction = 0; + + cur_subgraph++; + } + +#ifdef VERIFY_SAMPLES_F + std::cerr << "EdgeStore: Contracting to subgraphs " << cur_subgraph << " and above" << std::endl; + std::cerr << " num_edges = " << num_edges << std::endl; + std::cerr << " max_edges = " << max_edges << std::endl; + std::cerr << " num_inserted = " << num_inserted << std::endl; + std::cerr << " num_duplicate = " << num_duplicate << std::endl; + std::cerr << " num_returned = " << num_returned << std::endl; +#endif +} diff --git a/src/min_cut_sketch_alg.cpp b/src/min_cut_sketch_alg.cpp new file mode 100644 index 00000000..3c439ad4 --- /dev/null +++ b/src/min_cut_sketch_alg.cpp @@ -0,0 +1,306 @@ +#include "min_cut_sketch_alg.h" + +#include +#include +#include +#include +#include + +MinCutSketchAlg::MinCutSketchAlg(node_id_t _num_vertices, size_t _seed, MCAlgConfiguration _config) + : num_vertices(_num_vertices), + seed(_seed), + subgraph_seed(col_hash(&seed, sizeof(seed), seed)), + config(_config), + max_subgraphs(2 * log2(num_vertices)), + k(log2(num_vertices) / (config._epsilon * config._epsilon)), + cur_subgraphs(config._initial_subgraphs), + sketch_factor(1.3 / (config._epsilon * config._epsilon)), + sketch_samples(Sketch::calc_cc_samples(num_vertices, sketch_factor)), + buffer_elms(Sketch::estimate_bytes(Sketch::calc_vector_length(num_vertices), sketch_samples) / + sizeof(node_id_t)), + cc_sketches(new CCSketchAlg *[max_subgraphs]), + edge_store(seed, num_vertices, buffer_elms * sizeof(node_id_t), max_subgraphs, 1) { + if (cur_subgraphs > max_subgraphs) { + std::cerr << "WARNING: MinCutSketchAlg, initial_subgraphs > max_subgraphs. Setting to max." + << std::endl; + cur_subgraphs = max_subgraphs; + } + + cc_config.sketches_factor(sketch_factor); + + for (size_t i = 0; i < cur_subgraphs; i++) { + cc_sketches[i] = new CCSketchAlg(num_vertices, seed, cc_config); + if (i > 0) cc_sketches[i]->invalidate_dsu(); + } +} + +MinCutSketchAlg::~MinCutSketchAlg() { + for (size_t i = 0; i < cur_subgraphs; i++) { + delete cc_sketches[i]; + } + delete[] cc_sketches; + + if (delta_sketches != nullptr) delete[] delta_sketches; + if (thread_data != nullptr) delete[] thread_data; +} + +void MinCutSketchAlg::allocate_worker_memory(size_t _num_workers) { + num_workers = _num_workers; + for (size_t i = 0; i < cur_subgraphs; i++) { + cc_sketches[i]->allocate_worker_memory(num_workers); + } + + thread_data = new ThreadData[num_workers]; + for (size_t t = 0; t < num_workers; t++) { + thread_data[t].cc_buffers.resize(max_subgraphs); + for (size_t b = 1; b < cur_subgraphs; b++) { + thread_data[t].cc_buffers[b].resize(buffer_elms); + } + } +} + +// must hold advance_subgraph_lock when calling this function +void MinCutSketchAlg::advance_cur_subgraph(size_t new_cur_subgraphs) { + for (size_t i = cur_subgraphs; i < new_cur_subgraphs; i++) { + cc_sketches[i] = new CCSketchAlg(num_vertices, seed, cc_config); // TODO: DO WE NEED A DIFFERENT SEED? + cc_sketches[i]->allocate_worker_memory(num_workers); + cc_sketches[i]->invalidate_dsu(); + } + + for (size_t t = 0; t < num_workers; t++) { + for (size_t b = cur_subgraphs; b < new_cur_subgraphs; b++) { + thread_data[t].cc_buffers[b].resize(buffer_elms); + } + } + + cur_subgraphs = new_cur_subgraphs; +} + +void MinCutSketchAlg::pre_insert(GraphUpdate upd, node_id_t thr_id) { + // we just pre-insert to the first subgraph + // TODO: unless there's something more intelligent to do here at some point? + cc_sketches[0]->pre_insert(upd, thr_id); +} + +void MinCutSketchAlg::apply_update_batch(size_t thr_id, node_id_t src_vertex, + const std::vector &dst_vertices) { + assert(dst_vertices.size() <= buffer_elms); + + // everything goes in subgraph 0 + cc_sketches[0]->apply_update_batch(thr_id, src_vertex, dst_vertices); + + size_t num_mapped[max_subgraphs]; + std::fill(&num_mapped[0], &num_mapped[max_subgraphs - 1], 0); + + std::vector> &buffers = thread_data[thr_id].cc_buffers; + std::vector &edge_buf = thread_data[thr_id].edge_store_buffer; + edge_buf.resize(buffer_elms); + + size_t our_cur_subgraphs = cur_subgraphs; + + // map the updates to one of the subgraphs + for (size_t i = 0; i < dst_vertices.size(); i++) { + vec_t idx = concat_pairing_fn(src_vertex, dst_vertices[i]); + node_id_t subgraph_idx = SketchBucket::get_index_depth(idx, subgraph_seed, max_subgraphs - 1) + 1; + + if (subgraph_idx < our_cur_subgraphs) { + // goes in a sketch! + assert(num_mapped[subgraph_idx] < buffer_elms); + assert(buffers[subgraph_idx].size() == buffer_elms); + buffers[subgraph_idx][num_mapped[subgraph_idx]++] = dst_vertices[i]; + } else { + // goes in edge store! + assert(num_mapped[our_cur_subgraphs] < buffer_elms); + edge_buf[num_mapped[our_cur_subgraphs]++] = {subgraph_idx, dst_vertices[i]}; + } + } + + for (size_t i = 1; i < our_cur_subgraphs; i++) { + buffers[i].resize(num_mapped[i]); + cc_sketches[i]->apply_update_batch(thr_id, src_vertex, buffers[i]); + buffers[i].resize(buffer_elms); + } + + edge_buf.resize(num_mapped[our_cur_subgraphs]); + TaggedUpdateBatch batch = edge_store.insert_adj_edges(src_vertex, our_cur_subgraphs, edge_buf); + + while (batch.dsts_data.size() > 0) { + // we don't have a sketch for this subgraph yet! + if (batch.edge_store_subgraph > cur_subgraphs) { + advance_subgraph_lock.lock(); + // double check that we are the thread who will allocate next subgraph + if (batch.edge_store_subgraph > cur_subgraphs) { + advance_cur_subgraph(batch.edge_store_subgraph); + } + advance_subgraph_lock.unlock(); + } + + std::fill(&num_mapped[0], &num_mapped[max_subgraphs - 1], 0); + for (auto tagged_edge : batch.dsts_data) { + size_t subgraph = tagged_edge.subgraph; + assert(subgraph < cur_subgraphs); + + buffers[subgraph][num_mapped[subgraph]++] = tagged_edge.dst; + assert(num_mapped[subgraph] <= buffers[subgraph].capacity()); + + unlikely_if (num_mapped[subgraph] >= buffer_elms) { + cc_sketches[subgraph]->apply_update_batch(thr_id, batch.src, buffers[subgraph]); + num_mapped[subgraph] = 0; + } + } + + for (size_t i = 1; i < batch.edge_store_subgraph; i++) { + if (num_mapped[i] > 0) { + buffers[i].resize(num_mapped[i]); + cc_sketches[i]->apply_update_batch(thr_id, batch.src, buffers[i]); + buffers[i].resize(buffer_elms); + } + } + + // check if there are more contractions to perform + if (edge_store.contract_in_progress()) + batch = edge_store.vertex_advance_subgraph(cur_subgraphs); + else + batch.dsts_data.clear(); + } +} + +static MinCut run_viecut(node_id_t num_vertices, std::vector &edges) { + typedef VieCut::mutable_graph Graph; + typedef std::shared_ptr GraphPtr; + + // Create a VieCut graph + GraphPtr G = std::make_shared(); + G->start_construction(num_vertices, edges.size()); + + // Add edges to VieCut graph + for (auto edge : edges) { + G->new_edge(edge.src, edge.dst); + } + + // finish construction and compute degrees + // TODO: Don't know if degrees are necessary. Its in the VieCut code tho + G->finish_construction(); + G->computeDegrees(); + + // Perform the mincut computation + VieCut::EdgeWeight cut; + VieCut::minimum_cut* mc = new VieCut::viecut(); + cut = mc->perform_minimum_cut(G); + + // Return answer + std::set left; + std::set right; + + for (node_id_t i = 0; i < num_vertices; i++) { + if (G->getNodeInCut(i)) + left.insert(i); + else + right.insert(i); + } + + delete mc; + return {left, right, cut}; +} + +MinCut MinCutSketchAlg::calc_minimum_cut() { +#ifdef VERIFY_SAMPLES_F + create_subgraph_verifiers(); +#endif + + auto start = std::chrono::steady_clock::now(); + sf_total_duration = std::chrono::duration(0); + viecut_duration = std::chrono::duration(0); + + std::cout << "Performing minimum cut query" << std::endl; + for (size_t i = 0; i < cur_subgraphs; i++) { + std::cout << "Sketch " << i << " updates = " << cc_sketches[i]->get_num_updates() << std::endl; + } + + + // iterate over our subgraphs to find the correct value + for (size_t i = 0; i < cur_subgraphs; i++) { + auto sf_start = std::chrono::steady_clock::now(); + std::vector sfs = cc_sketches[i]->calc_disjoint_spanning_forests(k); + std::vector edges; + + for (auto& sf : sfs) { + auto& sf_edges = sf.get_edges(); + edges.insert(edges.end(), sf_edges.begin(), sf_edges.end()); + } + sf_total_duration += std::chrono::steady_clock::now() - sf_start; + + auto viecut_start =std::chrono::steady_clock::now(); + MinCut mc = run_viecut(num_vertices, edges); + size_t adjust_value = mc.value << i; + std::cout << "Subgraph: " << i + 1 << ", cut value = " << mc.value << ", k = " << k + << ", adjusted = " << adjust_value << std::endl; + viecut_duration += std::chrono::steady_clock::now() - viecut_start; + + if (mc.value < k) { + mc.value = adjust_value; + total_mc_duration = std::chrono::steady_clock::now() - start; + return mc; + } + } + + // pull from the adjacency list + std::vector adj_edges = edge_store.get_edges(); + MinCut mc = run_viecut(num_vertices, adj_edges); + std::cout << "Edge Store MinCut = " << mc.value << std::endl; + + // multiply the minimum cut by sampling rate + // -1 because edge store contains everything in remaining subgraphs, geometric series + mc.value <<= (cur_subgraphs - 1); + std::cout << "Adjusted = " << mc.value << std::endl; + total_mc_duration = std::chrono::steady_clock::now() - start; + return mc; +} + +#ifdef VERIFY_SAMPLES_F +void MinCutSketchAlg::set_verifier(std::unique_ptr _verifier) { + verifier = std::make_unique(*_verifier); + cc_sketches[0]->set_verifier(std::make_unique(*verifier)); +} + +void MinCutSketchAlg::create_subgraph_verifiers() { + cc_sketches[0]->set_verifier(std::make_unique(*verifier)); + std::vector> subgraph_verifiers; + + std::cout << "Creating: " << cur_subgraphs + 1 << " verifiers" << std::endl; + + for (size_t i = 0; i <= cur_subgraphs; i++) { + subgraph_verifiers.emplace_back(new GraphVerifier(num_vertices)); + } + + size_t subgraph_sizes[max_subgraphs]; + std::fill(&subgraph_sizes[0], &subgraph_sizes[max_subgraphs - 1], 0); + size_t non_zero = 0; + + std::vector> adj_mat = verifier->extract_adj_matrix(); + for (node_id_t i = 0; i < num_vertices; i++) { + for (node_id_t j = 0; j < num_vertices - i; j++) { + if (adj_mat[i][j]) { + node_id_t dst = i + j; + + non_zero++; + vec_t idx = concat_pairing_fn(i, dst); // edge is + node_id_t subgraph_idx = SketchBucket::get_index_depth(idx, subgraph_seed, max_subgraphs - 1) + 1; + if (subgraph_idx < cur_subgraphs) { + subgraph_verifiers[subgraph_idx]->edge_update({i, dst}); + subgraph_sizes[subgraph_idx]++; + } else { + subgraph_verifiers[cur_subgraphs]->edge_update({i, dst}); + } + } + } + } + + std::cout << "verifier subgraph 0 size = " << non_zero << std::endl; + for (size_t i = 1; i < cur_subgraphs; i++) { + cc_sketches[i]->set_verifier(std::make_unique(*subgraph_verifiers[i])); + std::cout << "verifier subgraph " << i << " size = " << subgraph_sizes[i] << std::endl; + } + adj_verifier = std::move(subgraph_verifiers[cur_subgraphs]); +} +#endif diff --git a/src/return_types.cpp b/src/return_types.cpp index f4c4998d..9689deb1 100644 --- a/src/return_types.cpp +++ b/src/return_types.cpp @@ -1,5 +1,6 @@ #include "return_types.h" +#include #include ConnectedComponents::ConnectedComponents(node_id_t num_vertices, @@ -39,3 +40,23 @@ SpanningForest::SpanningForest(node_id_t num_vertices, } } } + +const std::vector &SpanningForest::get_sorted_adjacency() { + if (has_adjacency) return sorted_adjacency; + + size_t num = edges.size(); + sorted_adjacency.resize(edges.size() * 2); + +#pragma omp parallel for + for (size_t i = 0; i < num; i++) { + sorted_adjacency[i] = edges[i]; + sorted_adjacency[i + num] = sorted_adjacency[i]; + std::swap(sorted_adjacency[i + num].src, sorted_adjacency[i + num].dst); + } + + // sort the edges + std::sort(sorted_adjacency.begin(), sorted_adjacency.end()); + + has_adjacency = true; + return sorted_adjacency; +} diff --git a/src/sketch.cpp b/src/sketch.cpp index ac674c5e..6a895e52 100644 --- a/src/sketch.cpp +++ b/src/sketch.cpp @@ -45,39 +45,52 @@ Sketch::Sketch(const Sketch &s) : seed(s.seed) { std::memcpy(buckets, s.buckets, bucket_array_bytes()); } -Sketch::~Sketch() { delete[] buckets; } +Sketch& Sketch::operator=(Sketch &&oth) { + seed = oth.seed; + cols_per_sample = oth.cols_per_sample; + num_columns = oth.num_columns; + bkt_per_col = oth.bkt_per_col; + num_buckets = oth.num_buckets; + buckets = oth.buckets; + + oth.buckets = nullptr; + + return *this; +} + +Sketch::~Sketch() { if (buckets != nullptr) delete[] buckets; } #ifdef L0_SAMPLING void Sketch::update(const vec_t update_idx) { - vec_hash_t checksum = Bucket_Boruvka::get_index_hash(update_idx, checksum_seed()); + vec_hash_t checksum = SketchBucket::get_index_hash(update_idx, checksum_seed()); // Update depth 0 bucket - Bucket_Boruvka::update(buckets[num_buckets - 1], update_idx, checksum); + SketchBucket::update(buckets[num_buckets - 1], update_idx, checksum); // Update higher depth buckets for (unsigned i = 0; i < num_columns; ++i) { - col_hash_t depth = Bucket_Boruvka::get_index_depth(update_idx, column_seed(i), bkt_per_col); + col_hash_t depth = SketchBucket::get_index_depth(update_idx, column_seed(i), bkt_per_col); likely_if(depth < bkt_per_col) { for (col_hash_t j = 0; j <= depth; ++j) { size_t bucket_id = i * bkt_per_col + j; - Bucket_Boruvka::update(buckets[bucket_id], update_idx, checksum); + SketchBucket::update(buckets[bucket_id], update_idx, checksum); } } } } #else // Use support finding algorithm instead. Faster but no guarantee of uniform sample. void Sketch::update(const vec_t update_idx) { - vec_hash_t checksum = Bucket_Boruvka::get_index_hash(update_idx, checksum_seed()); + vec_hash_t checksum = SketchBucket::get_index_hash(update_idx, checksum_seed()); // Update depth 0 bucket - Bucket_Boruvka::update(buckets[num_buckets - 1], update_idx, checksum); + SketchBucket::update(buckets[num_buckets - 1], update_idx, checksum); // Update higher depth buckets for (unsigned i = 0; i < num_columns; ++i) { - col_hash_t depth = Bucket_Boruvka::get_index_depth(update_idx, column_seed(i), bkt_per_col); + col_hash_t depth = SketchBucket::get_index_depth(update_idx, column_seed(i), bkt_per_col); size_t bucket_id = i * bkt_per_col + depth; likely_if(depth < bkt_per_col) { - Bucket_Boruvka::update(buckets[bucket_id], update_idx, checksum); + SketchBucket::update(buckets[bucket_id], update_idx, checksum); } } } @@ -102,13 +115,13 @@ SketchSample Sketch::sample() { if (buckets[num_buckets - 1].alpha == 0 && buckets[num_buckets - 1].gamma == 0) return {0, ZERO}; // the "first" bucket is deterministic so if all zero then no edges to return - if (Bucket_Boruvka::is_good(buckets[num_buckets - 1], checksum_seed())) + if (SketchBucket::is_good(buckets[num_buckets - 1], checksum_seed())) return {buckets[num_buckets - 1].alpha, GOOD}; for (size_t i = 0; i < cols_per_sample; ++i) { for (size_t j = 0; j < bkt_per_col; ++j) { size_t bucket_id = (i + first_column) * bkt_per_col + j; - if (Bucket_Boruvka::is_good(buckets[bucket_id], checksum_seed())) + if (SketchBucket::is_good(buckets[bucket_id], checksum_seed())) return {buckets[bucket_id].alpha, GOOD}; } } @@ -127,7 +140,7 @@ ExhaustiveSketchSample Sketch::exhaustive_sample() { unlikely_if (buckets[num_buckets - 1].alpha == 0 && buckets[num_buckets - 1].gamma == 0) return {ret, ZERO}; // the "first" bucket is deterministic so if zero then no edges to return - unlikely_if (Bucket_Boruvka::is_good(buckets[num_buckets - 1], checksum_seed())) { + unlikely_if (SketchBucket::is_good(buckets[num_buckets - 1], checksum_seed())) { ret.insert(buckets[num_buckets - 1].alpha); return {ret, GOOD}; } @@ -135,7 +148,7 @@ ExhaustiveSketchSample Sketch::exhaustive_sample() { for (size_t i = 0; i < cols_per_sample; ++i) { for (size_t j = 0; j < bkt_per_col; ++j) { size_t bucket_id = (i + first_column) * bkt_per_col + j; - unlikely_if (Bucket_Boruvka::is_good(buckets[bucket_id], checksum_seed())) { + unlikely_if (SketchBucket::is_good(buckets[bucket_id], checksum_seed())) { ret.insert(buckets[bucket_id].alpha); } } @@ -205,7 +218,7 @@ bool operator==(const Sketch &sketch1, const Sketch &sketch2) { std::ostream &operator<<(std::ostream &os, const Sketch &sketch) { Bucket bkt = sketch.buckets[sketch.num_buckets - 1]; - bool good = Bucket_Boruvka::is_good(bkt, sketch.checksum_seed()); + bool good = SketchBucket::is_good(bkt, sketch.checksum_seed()); vec_t a = bkt.alpha; vec_hash_t c = bkt.gamma; @@ -217,7 +230,7 @@ std::ostream &operator<<(std::ostream &os, const Sketch &sketch) { Bucket bkt = sketch.buckets[bucket_id]; vec_t a = bkt.alpha; vec_hash_t c = bkt.gamma; - bool good = Bucket_Boruvka::is_good(bkt, sketch.checksum_seed()); + bool good = SketchBucket::is_good(bkt, sketch.checksum_seed()); os << " a:" << a << " c:" << c << (good ? " good" : " bad") << std::endl; } diff --git a/src/sparse_sketch.cpp b/src/sparse_sketch.cpp new file mode 100644 index 00000000..bcf39d76 --- /dev/null +++ b/src/sparse_sketch.cpp @@ -0,0 +1,694 @@ +#include "sparse_sketch.h" +#include "dense_sketch.h" + +#include +#include +#include +#include + +SparseSketch::SparseSketch(vec_t vector_len, uint64_t seed, size_t _samples, size_t _cols) + : seed(seed), + num_samples(_samples), + cols_per_sample(_cols), + num_columns(cols_per_sample * num_samples), + bkt_per_col(calc_bkt_per_col(vector_len)) { + + if (num_columns > max_columns) { + throw std::invalid_argument("SparseSketch: Number of columns to high!"); + } + + // plus 1, deterministic bucket + num_buckets = calc_num_buckets(num_dense_rows); + buckets = new Bucket[num_buckets]; + upd_sparse_ptrs(); + + // initialize bucket values + for (size_t i = 0; i < num_buckets; ++i) { + buckets[i].alpha = 0; + buckets[i].gamma = 0; + } + + // initialize sparse bucket linked lists + // every bucket is currently free, so each points to next + for (size_t i = 0; i < sparse_capacity; i++) { + sparse_buckets[i].next = i + 1; + } + sparse_buckets[sparse_capacity - 1].next = uint16_t(-1); + + // initialize LL metadata + for (size_t i = 0; i < num_columns; i++) { + ll_metadata[i] = uint16_t(-1); // head of each column points nowhere (empty) + } + ll_metadata[num_columns] = 0; // free list head +} + +SparseSketch::SparseSketch(vec_t vector_len, uint64_t seed, std::istream &binary_in, + size_t num_buckets, size_t _samples, size_t _cols) + : seed(seed), + num_samples(_samples), + cols_per_sample(_cols), + num_columns(cols_per_sample * num_samples), + bkt_per_col(calc_bkt_per_col(vector_len)), + num_buckets(num_buckets) { + buckets = new Bucket[num_buckets]; + num_dense_rows = (num_buckets - sparse_data_size) / num_columns; + upd_sparse_ptrs(); + + // Read the serialized Sketch contents + binary_in.read((char *)buckets, bucket_array_bytes()); +} + +SparseSketch::SparseSketch(const SparseSketch &s) + : seed(s.seed), + num_samples(s.num_samples), + cols_per_sample(s.cols_per_sample), + num_columns(s.num_columns), + bkt_per_col(s.bkt_per_col), + num_buckets(s.num_buckets), + num_dense_rows(s.num_dense_rows) { + buckets = new Bucket[num_buckets]; + upd_sparse_ptrs(); + + std::memcpy(buckets, s.buckets, bucket_array_bytes()); +} + +SparseSketch::~SparseSketch() { + // std::cout << "Deleting sketch! buckets = " << buckets << std::endl; + delete[] buckets; +} + +// Helper functions for interfacing with SparseBuckets +void SparseSketch::dense_realloc(size_t new_num_dense_rows) { + // we are performing a reallocation + const size_t old_rows = num_dense_rows; + SparseBucket *old_sparse_pointer = sparse_buckets; + Bucket *old_buckets = buckets; + + if (new_num_dense_rows < min_num_dense_rows) { + throw std::runtime_error("new_num_dense_rows too small!"); + } + + // std::cerr << *this << std::endl; + + if (new_num_dense_rows < num_dense_rows) { + // std::cerr << "Shrinking to " << new_num_dense_rows << " from " << old_rows << std::endl; + // shrink dense region + // Scan over the rows we are removing and add all those buckets to sparse + for (size_t c = 0; c < num_columns; c++) { + for (size_t r = new_num_dense_rows; r < old_rows; r++) { + Bucket bkt = bucket(c, r); + if (!SketchBucket::is_empty(bkt)) { + uint16_t free_idx = claim_free_bucket(); + sparse_buckets[free_idx].row = r; + sparse_buckets[free_idx].bkt = bkt; + insert_to_ll_head(c, free_idx); + number_of_sparse_buckets += 1; + } + } + } + + // Allocate new memory + num_dense_rows = new_num_dense_rows; + num_buckets = calc_num_buckets(num_dense_rows); + buckets = new Bucket[num_buckets]; + } else { + // std::cerr << "Growing to " << new_num_dense_rows << " from " << old_rows << std::endl; + // grow dense region by 1 row + // Allocate new memory + num_dense_rows = new_num_dense_rows; + num_buckets = calc_num_buckets(num_dense_rows); + buckets = new Bucket[num_buckets]; + + // initialize new rows to zero + for (size_t c = 0; c < num_columns; c++) { + for (size_t r = old_rows; r < num_dense_rows; r++) { + buckets[position_func(c, r, num_dense_rows)] = {0, 0}; + } + } + } + upd_sparse_ptrs(); + + // Copy dense content + buckets[0] = old_buckets[0]; + for (size_t c = 0; c < num_columns; c++) { + for (size_t r = 0; r < std::min(num_dense_rows, old_rows); r++) { + buckets[position_func(c, r, num_dense_rows)] = old_buckets[position_func(c, r, old_rows)]; + } + } + // sparse contents + memcpy(sparse_buckets, old_sparse_pointer, + (sparse_data_size + ll_metadata_size) * sizeof(Bucket)); + + if (num_dense_rows > old_rows) { + // We growing + // Scan sparse buckets and move all updates of depth num_dense_rows-1 + // to the new dense row + for (size_t c = 0; c < num_columns; c++) { + while (ll_metadata[c] != uint16_t(-1) && sparse_buckets[ll_metadata[c]].row < num_dense_rows) { + // remove this bucket from column ll + uint16_t idx = remove_ll_head(c); + number_of_sparse_buckets -= 1; + + // add this bucket to dense region + bucket(c, sparse_buckets[idx].row) = sparse_buckets[idx].bkt; + + // add this sparse_bucket to free list + free_bucket(idx); + } + } + } + + // std::cerr << *this << std::endl; + + // 4. Clean up + delete[] old_buckets; +} + +void SparseSketch::reallocate_if_needed(int delta) { + // if we're currently adding something, don't shrink + if (delta == 1 && number_of_sparse_buckets <= num_columns / 4) { + return; + } + + // while we need to reallocate, attempt to do so. If realloc doesn't solve problem. Do it again. + while ((delta == -1 && number_of_sparse_buckets <= num_columns / 4 && + num_dense_rows > min_num_dense_rows) || + (delta == 1 && number_of_sparse_buckets == sparse_capacity)) { + if (number_of_sparse_buckets >= sparse_capacity) { + dense_realloc(num_dense_rows + 1); + } else { + dense_realloc(num_dense_rows - 1); + } + } +} + +// Update a bucket value +// Changes number_of_sparse_buckets as follows: +// +1 if we added a new bucket value +// 0 if the bucket was found and update (but not cleared) +// -1 if the bucket was found and cleared of all content +void SparseSketch::update_sparse(uint16_t col, const SparseBucket &to_add) { + uint16_t next_ptr = ll_metadata[col]; + uint16_t prev = uint16_t(-1); + while (next_ptr != uint16_t(-1)) { + if (sparse_buckets[next_ptr].row == to_add.row) { + bool removed = merge_sparse_bkt(next_ptr, to_add, prev, col); + if (removed) { + number_of_sparse_buckets -= 1; + reallocate_if_needed(-1); + } + return; + } else if (sparse_buckets[next_ptr].row > to_add.row) { + break; + } + prev = next_ptr; + next_ptr = sparse_buckets[next_ptr].next; + } + + // pull a bucket off the free list and set it equal to to_add + uint16_t free_bucket = claim_free_bucket(); + // std::cerr << "free bucket = " << size_t(free_bucket) << std::endl; + // std::cerr << "next bucket = " << size_t(next_ptr) << std::endl; + // std::cerr << "free head = " << size_t(ll_metadata[num_columns]) << std::endl; + + // update bucket + sparse_buckets[free_bucket] = to_add; + number_of_sparse_buckets += 1; + // std::cerr << "new bucket " << size_t(sparse_buckets[free_bucket].row) << " n = " << size_t(sparse_buckets[free_bucket].next) << std::endl; + + // update column ll + if (prev == uint16_t(-1)) { + insert_to_ll_head(col, free_bucket); + // std::cerr << "Set column head to new bucket " << size_t(ll_metadata[col]) << std::endl; + } else { + insert_to_ll(free_bucket, sparse_buckets[prev]); + // std::cerr << "Placed new bucket in column " << size_t(prev) << "->" << size_t(sparse_buckets[prev].next) << "->" << size_t(sparse_buckets[free_bucket].next) << std::endl; + } + + reallocate_if_needed(1); +} + +// sample a good bucket from the sparse region if one exists. +// Additionally, specify the column to query from +SketchSample SparseSketch::sample_sparse(size_t first_col, size_t end_col) { + // std::cerr << "sample_sparse" << std::endl; + for (size_t c = first_col; c < end_col; c++) { + uint16_t idx = ll_metadata[c]; + while (idx != uint16_t(-1)) { + if (SketchBucket::is_good(sparse_buckets[idx].bkt, checksum_seed())) { + return {sparse_buckets[idx].bkt.alpha, GOOD}; + } + idx = sparse_buckets[idx].next; + } + } + + // We could not find a good bucket + // std::cout << "Sketch FAIL" << std::endl; + return {0, FAIL}; +} + +void SparseSketch::update(const vec_t update_idx) { + vec_hash_t checksum = SketchBucket::get_index_hash(update_idx, checksum_seed()); + + // Update depth 0 bucket + SketchBucket::update(deterministic_bucket(), update_idx, checksum); + SketchBucket::Depths depths; + + // Update higher depth buckets + for (size_t i = 0; i < num_columns - 1; i += 2) { + depths = SketchBucket::get_index_depths(update_idx, column_seed(i), bkt_per_col); + for (size_t j = 0; j < 2; j++) { + col_hash_t depth = depths[j]; + likely_if(depth < bkt_per_col) { + likely_if(depth < num_dense_rows) { + SketchBucket::update(bucket(i + j, depth), update_idx, checksum); + } else { + update_sparse(i + j, {uint16_t(-1), uint8_t(depth), {update_idx, checksum}}); + } + } + } + } + if ((num_columns & 0x1) == 1) { + size_t col = num_columns - 1; + + size_t depth = SketchBucket::get_index_depth(update_idx, column_seed(col), bkt_per_col); + likely_if(depth < bkt_per_col) { + likely_if(depth < num_dense_rows) { + SketchBucket::update(bucket(col, depth), update_idx, checksum); + } else { + update_sparse(col, {uint16_t(-1), uint8_t(depth), {update_idx, checksum}}); + } + } + } +} + +// TODO: Switch the L0_SAMPLING flag to instead affect query procedure. +// (Only use deepest bucket. We don't need the alternate update procedure in the code anymore.) + +void SparseSketch::zero_contents() { + for (size_t i = 0; i < num_buckets; i++) { + buckets[i].alpha = 0; + buckets[i].gamma = 0; + } + + // initialize sparse bucket linked lists + // every bucket is currently free, so each points to next + for (size_t i = 0; i < sparse_capacity; i++) { + sparse_buckets[i].next = i + 1; + } + sparse_buckets[sparse_capacity - 1].next = uint16_t(-1); + + // initialize LL metadata + for (size_t i = 0; i < num_columns; i++) { + ll_metadata[i] = uint16_t(-1); // head of each column points nowhere (empty) + } + ll_metadata[num_columns] = 0; // free list head + + reset_sample_state(); + number_of_sparse_buckets = 0; + // if (num_dense_rows > min_num_dense_rows + 4) + // dense_realloc(min_num_dense_rows); +} + +SketchSample SparseSketch::sample() { + if (sample_idx >= num_samples) { + throw OutOfSamplesException(seed, num_samples, sample_idx); + } + + size_t idx = sample_idx++; + size_t first_column = idx * cols_per_sample; + + // std::cout << "Sampling sketch" << std::endl; + // std::cout << "first_col = " << first_column << std::endl; + // std::cout << "end_col = " << first_column + cols_per_sample << std::endl; + // std::cout << *this << std::endl; + + if (SketchBucket::is_empty(deterministic_bucket())) { + // std::cout << "ZERO!" << std::endl; + return {0, ZERO}; // the "first" bucket is deterministic so if all zero then no edges to return + } + + if (SketchBucket::is_good(deterministic_bucket(), checksum_seed())) { + // std::cout << "Deterministic GOOD" << std::endl; + return {deterministic_bucket().alpha, GOOD}; + } + + // Sample sparse region + SketchSample sample = sample_sparse(first_column, first_column + cols_per_sample); + if (sample.result == GOOD) { + return sample; + } + + // if dense region is densely populated then only check the "deepest" few rows + int dense_row_min = 0; + if (number_of_sparse_buckets > num_columns || num_dense_rows > min_num_dense_rows) { + dense_row_min = num_dense_rows - num_dense_to_sample; + } + + for (size_t c = 0; c < cols_per_sample; ++c) { + for (int r = num_dense_rows - 1; r >= dense_row_min; --r) { + if (SketchBucket::is_good(bucket(c + first_column, r), checksum_seed())) { + // std::cout << "Found GOOD dense bucket" << std::endl; + return {bucket(c + first_column, r).alpha, GOOD}; + } + } + } + + // Sample sparse region + // std::cout << "Sketch is bad" << std::endl; + // std::cout << *this << std::endl; + return {0, FAIL}; +} + +ExhaustiveSketchSample SparseSketch::exhaustive_sample() { + if (sample_idx >= num_samples) { + throw OutOfSamplesException(seed, num_samples, sample_idx); + } + std::vector ret; + + size_t idx = sample_idx++; + size_t first_column = idx * cols_per_sample; + + unlikely_if (SketchBucket::is_empty(deterministic_bucket())) + return {ret, ZERO}; // the "first" bucket is deterministic so if zero then no edges to return + + unlikely_if (SketchBucket::is_good(deterministic_bucket(), checksum_seed())) { + ret.push_back(deterministic_bucket().alpha); + return {ret, GOOD}; + } + + for (size_t c = 0; c < cols_per_sample; ++c) { + for (size_t r = 0; r < num_dense_rows; ++r) { + unlikely_if (SketchBucket::is_good(bucket(c + first_column, r), checksum_seed())) { + ret.push_back(bucket(c + first_column, r).alpha); + } + } + } + + // TODO: How do we do exhaustive sampling properly here? + SketchSample sample = sample_sparse(first_column, first_column + cols_per_sample); + if (sample.result == GOOD) { + ret.push_back(sample.idx); + } + + unlikely_if (ret.size() == 0) + return {ret, FAIL}; + return {ret, GOOD}; +} + +void SparseSketch::merge_sparse_column(const SparseBucket *oth_sparse_buckets, + const uint16_t *oth_ll_metadata, size_t col) { + // std::cerr << "Merging sparse column: " << col << std::endl; + uint16_t oth_idx = oth_ll_metadata[col]; + uint16_t our_idx = ll_metadata[col]; + uint16_t prev = uint16_t(-1); + + // merge column until one runs out + while (oth_idx != uint16_t(-1) && our_idx != uint16_t(-1)) { + const SparseBucket& oth_sparse = oth_sparse_buckets[oth_idx]; + SparseBucket& our_sparse = sparse_buckets[our_idx]; + + if (oth_sparse.row < num_dense_rows) { + // just merge into dense! + bucket(col, oth_sparse.row).alpha ^= oth_sparse.bkt.alpha; + bucket(col, oth_sparse.row).gamma ^= oth_sparse.bkt.gamma; + oth_idx = oth_sparse.next; + continue; + } + + if (oth_sparse.row > our_sparse.row) { + // skip our bucket, sparse doesn't have anything to match it + prev = our_idx; + our_idx = our_sparse.next; + } else if (oth_sparse.row < our_sparse.row) { + // oth has a bucket we don't have, insert it + uint16_t free_bucket = claim_free_bucket(); + // std::cerr << "ours = " << size_t(our_idx) << " free = " << size_t(free_bucket) << std::endl; + + sparse_buckets[free_bucket] = oth_sparse; + if (prev == uint16_t(-1)) { + insert_to_ll_head(col, free_bucket); + } else { + insert_to_ll(free_bucket, sparse_buckets[prev]); + } + number_of_sparse_buckets += 1; + reallocate_if_needed(1); + oth_idx = oth_sparse.next; + prev = free_bucket; + if (ll_metadata[col] == uint16_t(-1) || ll_metadata[col] == our_idx) prev = uint16_t(-1); + } else { + // they are equal, merge them! + uint16_t our_next = our_sparse.next; + uint16_t oth_next = oth_sparse.next; + bool removed = merge_sparse_bkt(our_idx, oth_sparse, prev, col); + if (removed) { + number_of_sparse_buckets -= 1; + reallocate_if_needed(-1); + } else { + prev = our_idx; + } + oth_idx = oth_next; + our_idx = our_next; + } + } + + // if there's more in the other column, merge that stuff in + while (oth_idx != uint16_t(-1)) { + const SparseBucket& oth_sparse = oth_sparse_buckets[oth_idx]; + if (oth_sparse.row < num_dense_rows) { + bucket(col, oth_sparse.row).alpha ^= oth_sparse.bkt.alpha; + bucket(col, oth_sparse.row).gamma ^= oth_sparse.bkt.gamma; + oth_idx = oth_sparse.next; + continue; + } + + uint16_t free_bucket = claim_free_bucket(); + sparse_buckets[free_bucket] = oth_sparse; + if (prev == uint16_t(-1)) { + insert_to_ll_head(col, free_bucket); + } else { + insert_to_ll(free_bucket, sparse_buckets[prev]); + } + number_of_sparse_buckets += 1; + reallocate_if_needed(1); + prev = free_bucket; + if (ll_metadata[col] == uint16_t(-1)) prev = uint16_t(-1); + oth_idx = oth_sparse.next; + } +} + +void SparseSketch::merge(const SparseSketch &other) { + // std::cerr << "PERFORMING A MERGE" << std::endl; + // std::cerr << *this << std::endl; + + // std::cerr << "MERGE SKETCH" << std::endl; + // std::cerr << other << std::endl; + + // merge the deterministic bucket + deterministic_bucket().alpha ^= other.deterministic_bucket().alpha; + deterministic_bucket().gamma ^= other.deterministic_bucket().gamma; + + // merge all dense buckets from other sketch into this one + for (size_t c = 0; c < num_columns; c++) { + for (size_t r = 0; r < other.num_dense_rows; ++r) { + if (r < num_dense_rows) { + bucket(c, r).alpha ^= other.bucket(c, r).alpha; + bucket(c, r).gamma ^= other.bucket(c, r).gamma; + } else if (!SketchBucket::is_empty(other.bucket(c, r))) { + SparseBucket sparse_bkt; + sparse_bkt.row = r; + sparse_bkt.bkt = other.bucket(c, r); + update_sparse(c, sparse_bkt); + } + } + } + + // Merge all sparse buckets from other sketch into this one + for (size_t c = 0; c < num_columns; c++) { + merge_sparse_column(other.sparse_buckets, other.ll_metadata, c); + } +} + +void SparseSketch::range_merge(const SparseSketch &other, size_t start_sample, size_t n_samples) { + if (start_sample + n_samples > num_samples) { + assert(false); + sample_idx = num_samples; // sketch is in a fail state! + return; + } + // std::cerr << "SKETCH BEFORE MERGE" << std::endl; + // std::cerr << *this << std::endl; + + // std::cerr << "SKETCH WE MERGE WITH" << std::endl; + // std::cerr << other << std::endl; + + // update sample idx to point at beginning of this range if before it + sample_idx = std::max(sample_idx, start_sample); + + // Columns we be merging + size_t start_column = start_sample * cols_per_sample; + size_t end_column = (start_sample + n_samples) * cols_per_sample; + + // merge deterministic buffer + deterministic_bucket().alpha ^= other.deterministic_bucket().alpha; + deterministic_bucket().gamma ^= other.deterministic_bucket().gamma; + + // merge all their dense buckets into us + for (size_t c = start_column; c < end_column; c++) { + for (size_t r = 0; r < other.num_dense_rows; r++) { + if (r < num_dense_rows) { + bucket(c, r).alpha ^= other.bucket(c, r).alpha; + bucket(c, r).gamma ^= other.bucket(c, r).gamma; + } else if (!SketchBucket::is_empty(other.bucket(c, r))) { + SparseBucket sparse_bkt; + sparse_bkt.row = r; + sparse_bkt.bkt = other.bucket(c, r); + update_sparse(c, sparse_bkt); + } + } + } + + // Merge all sparse buckets from other sketch into this one + for (size_t c = start_column; c < end_column; c++) { + merge_sparse_column(other.sparse_buckets, other.ll_metadata, c); + } + // std::cerr << "SKETCH AFTER MERGE" << std::endl; + // std::cerr << *this << std::endl; +} + +void SparseSketch::merge_raw_bucket_buffer(const Bucket *raw_buckets, size_t n_raw_buckets) { + size_t raw_rows = (n_raw_buckets - sparse_data_size - ll_metadata_size - 1) / num_columns; + const SparseBucket *raw_sparse = (const SparseBucket *) &raw_buckets[calc_sparse_index(raw_rows)]; + const uint16_t *raw_metadata = (const uint16_t *) &raw_buckets[calc_metadata_index(raw_rows)]; + + deterministic_bucket().alpha ^= raw_buckets[0].alpha; + deterministic_bucket().gamma ^= raw_buckets[0].gamma; + + for (size_t c = 0; c < num_columns; c++) { + for (size_t r = 0; r < raw_rows; r++) { + if (r < num_dense_rows) { + bucket(c, r).alpha ^= raw_buckets[position_func(c, r, raw_rows)].alpha; + bucket(c, r).gamma ^= raw_buckets[position_func(c, r, raw_rows)].gamma; + } else if (!SketchBucket::is_empty( + raw_buckets[position_func(c, r, raw_rows)])) { + SparseBucket sparse_bkt; + sparse_bkt.row = r; + sparse_bkt.bkt = raw_buckets[position_func(c, r, raw_rows)]; + update_sparse(c, sparse_bkt); + } + } + } + + // Merge all sparse buckets from other sketch into this one + for (size_t c = 0; c < num_columns; c++) { + merge_sparse_column(raw_sparse, raw_metadata, c); + } +} + +void SparseSketch::serialize(std::ostream &binary_out) const { + binary_out.write((char*) buckets, bucket_array_bytes()); +} + +bool operator==(const SparseSketch &sketch1, const SparseSketch &sketch2) { + if (sketch1.num_buckets != sketch2.num_buckets || sketch1.seed != sketch2.seed) + return false; + + return memcmp(sketch1.buckets, sketch2.buckets, + sketch1.bucket_array_bytes() - sketch1.ll_metadata_size * sizeof(Bucket)) == 0; +} + +bool operator==(const SparseSketch& sparse, const DenseSketch& dense) { + if (sparse.num_columns != dense.num_columns) return false; + if (sparse.num_dense_rows > dense.bkt_per_col) return false; + + for (size_t c = 0; c < sparse.num_columns; c++) { + for (size_t r = 0; r < sparse.num_dense_rows; r++) { + if (sparse.bucket(c, r).alpha != dense.bucket(c, r).alpha || + sparse.bucket(c, r).gamma != dense.bucket(c, r).gamma) { + std::cout << "Dense bucket " << c << "," << r << " not equal!" << std::endl; + return false; + } + + } + } + + const auto sparse_buckets = sparse.sparse_buckets; + for (size_t c = 0; c < sparse.num_columns; c++) { + SparseBucket sparse_bkt = sparse_buckets[sparse.ll_metadata[c]]; + for (size_t r = sparse.num_dense_rows; r < dense.bkt_per_col; r++) { + if (sparse_bkt.row != r) { + if (!SketchBucket::is_empty(dense.bucket(c,r)) != 0) { + std::cout << "expected " << c << "," << r << " empty. it is not." << std::endl; + return false; + } + } else { + if (sparse_bkt.bkt.alpha != dense.bucket(c, r).alpha || + sparse_bkt.bkt.gamma != dense.bucket(c, r).gamma) { + std::cout << "sparse bucket " << c << "," << r << " not equal to dense" << std::endl; + return false; + } + if (sparse_bkt.next == uint16_t(-1)) { + sparse_bkt = {uint16_t(-1), 0, {0,0}}; + } else { + sparse_bkt = sparse_buckets[sparse_bkt.next]; + } + } + } + } + + return true; +} + +std::ostream &operator<<(std::ostream &os, const SparseSketch &sketch) { + Bucket bkt = sketch.deterministic_bucket(); + bool good = SketchBucket::is_good(bkt, sketch.checksum_seed()); + vec_t a = bkt.alpha; + vec_hash_t c = bkt.gamma; + + os << " a:" << a << " c:" << c << (good ? " good" : " bad") << std::endl; + + os << "Number of dense rows = " << sketch.num_dense_rows << std::endl; + for (unsigned i = 0; i < sketch.num_columns; ++i) { + for (unsigned j = 0; j < sketch.num_dense_rows; ++j) { + Bucket bkt = sketch.bucket(i, j); + vec_t a = bkt.alpha; + vec_hash_t c = bkt.gamma; + bool good = SketchBucket::is_good(bkt, sketch.checksum_seed()); + + os << " a:" << a << " c:" << c << (good ? " good" : " bad") << std::endl; + } + os << std::endl; + } + + os << "Sparse Buckets" << std::endl; + const auto sparse_buckets = sketch.sparse_buckets; + for (size_t c = 0; c < sketch.num_columns; c++) { + uint16_t idx = sketch.ll_metadata[c]; + while (idx != uint16_t(-1)) { + bool good = SketchBucket::is_good(sparse_buckets[idx].bkt, sketch.checksum_seed()); + os << "i: " << size_t(idx) << " n: " << size_t(sparse_buckets[idx].next) << " p:" << c << ", " + << size_t(sparse_buckets[idx].row) << " := a:" << sparse_buckets[idx].bkt.alpha + << " c:" << sparse_buckets[idx].bkt.gamma << (good ? " good" : " bad") << std::endl; + if (idx == sketch.sparse_buckets[idx].next) { + os << "LL error!" << std::endl; + return os; + } + idx = sketch.sparse_buckets[idx].next; + } + } + os << "Free Buckets" << std::endl; + uint16_t idx = sketch.ll_metadata[sketch.num_columns]; + while (idx != uint16_t(-1)) { + bool good = SketchBucket::is_good(sparse_buckets[idx].bkt, sketch.checksum_seed()); + os << "i: " << size_t(idx) << " n: " << size_t(sparse_buckets[idx].next) << " r:" + << size_t(sparse_buckets[idx].row) << " := a:" << sparse_buckets[idx].bkt.alpha + << " c:" << sparse_buckets[idx].bkt.gamma << (good ? " good" : " bad") << std::endl; + if (idx == sketch.sparse_buckets[idx].next) { + os << "LL error!" << std::endl; + return os; + } + idx = sketch.sparse_buckets[idx].next; + } + + os << std::endl; + return os; +} diff --git a/src/util.cpp b/src/util.cpp index 9854dbaa..b295c785 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -8,8 +8,9 @@ typedef uint32_t ul; typedef uint64_t ull; -constexpr ull ULLMAX = std::numeric_limits::max(); -constexpr uint8_t num_bits = sizeof(node_id_t) * 8; +static constexpr ull ULLMAX = std::numeric_limits::max(); +static constexpr uint8_t num_bits = sizeof(node_id_t) * 8; +static constexpr size_t mask = (size_t(1) << num_bits) - 1; unsigned long long int double_to_ull(double d, double epsilon) { return (unsigned long long) (d + epsilon); @@ -42,16 +43,13 @@ Edge inv_nondir_non_self_edge_pairing_fn(uint64_t idx) { return {(node_id_t)i, (node_id_t)j}; } +// smaller node_id concatenated with larger node_id edge_id_t concat_pairing_fn(node_id_t i, node_id_t j) { - // swap i,j if necessary - if (i > j) { - std::swap(i,j); - } - return ((edge_id_t)i << num_bits) | j; + return ((edge_id_t)std::min(i, j) << num_bits) | std::max(i, j); } Edge inv_concat_pairing_fn(ull idx) { - node_id_t j = idx & 0xFFFFFFFF; + node_id_t j = idx & mask; node_id_t i = idx >> num_bits; return {i, j}; } diff --git a/test/cc_alg_test.cpp b/test/cc_alg_test.cpp index 5f395b01..d2b5dfe2 100644 --- a/test/cc_alg_test.cpp +++ b/test/cc_alg_test.cpp @@ -299,7 +299,7 @@ TEST(CCAlgTest, InsertOnlyStream) { TEST(CCAlgTest, MTStreamWithMultipleQueries) { for (int t = 1; t <= 3; t++) { - auto driver_config = DriverConfiguration().gutter_sys(STANDALONE); + auto driver_config = DriverConfiguration().gutter_sys(STANDALONE).stream_threads(4); const std::string fname = __FILE__; size_t pos = fname.find_last_of("\\/"); @@ -313,7 +313,7 @@ TEST(CCAlgTest, MTStreamWithMultipleQueries) { std::cerr << num_nodes << " " << num_edges << std::endl; CCSketchAlg cc_alg{num_nodes, get_seed()}; - GraphSketchDriver driver(&cc_alg, &stream, driver_config, 4); + GraphSketchDriver driver(&cc_alg, &stream, driver_config); GraphVerifier verify(num_nodes); size_t num_queries = 10; diff --git a/test/edge_store_test.cpp b/test/edge_store_test.cpp new file mode 100644 index 00000000..fde58aa5 --- /dev/null +++ b/test/edge_store_test.cpp @@ -0,0 +1,240 @@ +#include "edge_store.h" +#include "bucket.h" +#include "util.h" + +#include +#include +#include + +static size_t get_seed() { + auto now = std::chrono::high_resolution_clock::now(); + return std::chrono::duration_cast(now.time_since_epoch()).count(); +} + +TEST(EdgeStoreTest, no_contract) { + size_t nodes = 1024; + size_t skt_bytes = 1 << 15; // artificially large + EdgeStore edge_store(get_seed(), nodes, skt_bytes, 20); + + std::set edges_added; + + for (size_t i = 0; i < nodes; i++) { + std::vector dsts; + for (size_t j = 0; j < i; j++) { + dsts.push_back(j); + ASSERT_TRUE(edges_added.insert({std::min(i, j), std::max(i, j)}).second); + } + auto more_upds = edge_store.insert_adj_edges(i, dsts); + ASSERT_EQ(more_upds.dsts_data.size(), 0); + } + + ASSERT_EQ(edge_store.get_num_edges(), edges_added.size()); + ASSERT_EQ(edge_store.get_first_store_subgraph(), 0); + + std::vector edges = edge_store.get_edges(); + ASSERT_EQ(edges.size(), edges_added.size()); + + for (auto edge : edges) { + node_id_t src = std::min(edge.src, edge.dst); + node_id_t dst = std::max(edge.src, edge.dst); + ASSERT_NE(edges_added.find({src, dst}), edges_added.end()); + } +} + +TEST(EdgeStoreTest, test_sort) { + size_t nodes = 1024; + size_t skt_bytes = 1 << 15; // artificially large + EdgeStore edge_store(get_seed(), nodes, skt_bytes, 20); + + std::set edges_added; + + std::vector dsts1{1,5,5,10,15,15,15,20,25,30,30,30,40,40}; + std::vector dsts2{3,15,20,100,101,102,103}; + std::set expect{1,3,10,25,30,100,101,102,103}; + + edge_store.insert_adj_edges(0, dsts1); + edge_store.insert_adj_edges(0, dsts2); + + std::vector edges = edge_store.get_edges(); + ASSERT_EQ(edge_store.get_num_edges(), 9); + ASSERT_EQ(edges.size(), 9); + + for (size_t i = 0; i < 9; i++) { + node_id_t src = edges[i].src; + node_id_t dst = edges[i].dst; + ASSERT_EQ(src, 0); + ASSERT_NE(expect.find(dst), expect.end()); + } +} + +TEST(EdgeStoreTest, no_contract_dupl) { + size_t nodes = 1024; + size_t skt_bytes = 1 << 20; // artificially large + EdgeStore edge_store(get_seed(), nodes, skt_bytes, 20); + + std::set edges_added; + + for (size_t i = 0; i < nodes; i++) { + std::vector dsts; + for (size_t j = 0; j < i; j++) { + dsts.push_back(j); + ASSERT_TRUE(edges_added.insert({i, j}).second); + } + auto more_upds = edge_store.insert_adj_edges(i, dsts); + ASSERT_EQ(more_upds.dsts_data.size(), 0); + } + + // remove num_nodes edges from graph + for (size_t i = 0; i < nodes; i++) { + auto it = edges_added.begin(); + edge_store.insert_adj_edges(it->src, std::vector{it->dst}); + edges_added.erase(it); + } + + ASSERT_EQ(edge_store.get_num_edges(), edges_added.size()); + ASSERT_EQ(edge_store.get_first_store_subgraph(), 0); + + std::vector edges = edge_store.get_edges(); + ASSERT_EQ(edges.size(), edges_added.size()); + + for (auto edge : edges) { + ASSERT_NE(edges_added.find(edge), edges_added.end()); + } +} + +TEST(EdgeStoreTest, contract) { + size_t nodes = 1024; + size_t skt_bytes = 1 << 9; // small enough to likely contract thrice + size_t num_subgraphs = 20; + size_t seed = get_seed(); + EdgeStore edge_store(seed, nodes, skt_bytes, num_subgraphs, 1); + + std::set edges_added; + + size_t num_returned[6] = {0, 0, 0, 0, 0, 0}; + size_t num_in_subgraphs[6] = {0, 0, 0, 0, 0, 0}; + + for (size_t i = 0; i < nodes; i++) { + std::vector dsts; + for (size_t j = i + 1; j < nodes; j++) { + node_id_t src = std::min(i, j); + node_id_t dst = std::max(i, j); + auto idx = concat_pairing_fn(src, dst); + size_t depth = SketchBucket::get_index_depth(idx, seed, num_subgraphs - 1) + 1; + + ++num_in_subgraphs[std::min(size_t(5), depth)]; + + if (depth >= edge_store.get_first_store_subgraph()) { + dsts.push_back({depth, j}); + } else { + ++num_returned[depth]; + } + ASSERT_TRUE(edges_added.insert({src, dst}).second); + } + auto more_upds = edge_store.insert_adj_edges(i, edge_store.get_first_store_subgraph(), dsts); + node_id_t src = more_upds.src; + for (auto dst_data : more_upds.dsts_data) { + ++num_returned[dst_data.subgraph]; + node_id_t s = std::min(src, dst_data.dst); + node_id_t d = std::max(src, dst_data.dst); + ASSERT_NE(edges_added.find({s, d}), edges_added.end()); + } + } + + while (edge_store.contract_in_progress()) { + auto more_upds = edge_store.vertex_advance_subgraph(edge_store.get_first_store_subgraph()); + node_id_t src = more_upds.src; + for (auto dst_data : more_upds.dsts_data) { + ++num_returned[dst_data.subgraph]; + node_id_t s = std::min(src, dst_data.dst); + node_id_t d = std::max(src, dst_data.dst); + ASSERT_NE(edges_added.find({s, d}), edges_added.end()); + } + } + + for (size_t i = 0; i < 6; i++) { + std::cerr << num_returned[i] << " vs " << num_in_subgraphs[i] << std::endl; + } + + for (size_t i = 0; i < edge_store.get_first_store_subgraph(); i++) { + ASSERT_EQ(num_returned[i], num_in_subgraphs[i]); + } + + size_t expected_es_edges = 0; + for (size_t i = edge_store.get_first_store_subgraph(); i <= 5; i++) { + expected_es_edges += num_in_subgraphs[i]; + } + + std::vector edges = edge_store.get_edges(); + ASSERT_EQ(edges.size(), expected_es_edges); + + for (auto edge : edges) { + node_id_t src = std::min(edge.src, edge.dst); + node_id_t dst = std::max(edge.src, edge.dst); + ASSERT_NE(edges_added.find({src, dst}), edges_added.end()); + } +} + +TEST(EdgeStoreTest, contract_parallel) { + size_t nodes = 1024; + size_t skt_bytes = 1 << 9; // small enough to likely contract twice + size_t num_subgraphs = 20; + size_t seed = get_seed(); + EdgeStore edge_store(seed, nodes, skt_bytes, num_subgraphs); + + std::atomic num_returned[6] = {0, 0, 0, 0, 0, 0}; + std::atomic num_in_subgraphs[6] = {0, 0, 0, 0, 0, 0}; + +#pragma omp parallel for + for (size_t i = 0; i < nodes; i++) { + std::vector dsts; + size_t edge_store_subgraph = edge_store.get_first_store_subgraph(); + for (size_t j = i + 1; j < nodes; j++) { + node_id_t src = std::min(i, j); + node_id_t dst = std::max(i, j); + auto idx = concat_pairing_fn(src, dst); + size_t depth = SketchBucket::get_index_depth(idx, seed, num_subgraphs - 1) + 1; + + ++num_in_subgraphs[std::min(size_t(5), depth)]; + + if (depth >= edge_store_subgraph) { + dsts.push_back({depth, j}); + } else { + ++num_returned[depth]; + } + } + auto more_upds = edge_store.insert_adj_edges(i, edge_store_subgraph, dsts); + for (auto dst_data : more_upds.dsts_data) { + ++num_returned[dst_data.subgraph]; + } + } + +#pragma omp parallel + { + while (edge_store.contract_in_progress()) { + auto more_upds = edge_store.vertex_advance_subgraph(edge_store.get_first_store_subgraph()); + for (auto dst_data : more_upds.dsts_data) { + ++num_returned[dst_data.subgraph]; + } + } + } + + for (size_t i = 0; i < 6; i++) { + std::cerr << num_returned[i] << " vs " << num_in_subgraphs[i] << std::endl; + } + + std::vector edges = edge_store.get_edges(); + std::cerr << "edge store size = " << edges.size() << std::endl; + + for (size_t i = 0; i < edge_store.get_first_store_subgraph(); i++) { + ASSERT_EQ(num_returned[i], num_in_subgraphs[i]); + } + + size_t expected_es_edges = 0; + for (size_t i = edge_store.get_first_store_subgraph(); i <= 5; i++) { + expected_es_edges += num_in_subgraphs[i]; + } + + + ASSERT_EQ(edges.size(), expected_es_edges); +} diff --git a/test/min_cut_test.cpp b/test/min_cut_test.cpp new file mode 100644 index 00000000..06aebd8e --- /dev/null +++ b/test/min_cut_test.cpp @@ -0,0 +1,104 @@ +#include +#include +#include +#include +#include + +#include +#include + +#include "min_cut_sketch_alg.h" +#include "graph_sketch_driver.h" +#include "graph_verifier.h" + +// helper function to generate a dynamic binary stream and its cumulative insert only stream +void generate_stream(size_t seed, node_id_t num_vertices, double density, std::string stream_name) { + // generate new stream files + StaticErdosGenerator stream(seed, num_vertices, density); + stream.to_binary_file(stream_name); +} + +static size_t get_seed() { + auto now = std::chrono::high_resolution_clock::now(); + size_t s = std::chrono::duration_cast(now.time_since_epoch()).count(); + std::cout << "Seed = " << s << std::endl; + return s; +} + +// Test for constructing mincut and driver. Just to ensure everything work +TEST(MinCutTest, Construction) { + const std::string fname = __FILE__; + size_t pos = fname.find_last_of("\\/"); + const std::string curr_dir = (std::string::npos == pos) ? "" : fname.substr(0, pos); + AsciiFileStream stream{curr_dir + "/res/multiples_graph_1024.txt", false}; + + MinCutSketchAlg mc_alg(1024, get_seed()); + GraphSketchDriver driver(&mc_alg, &stream, DriverConfiguration()); +} + +TEST(MinCutTest, DisconnectedMinCut) { + auto driver_config = DriverConfiguration(); + const std::string fname = __FILE__; + size_t pos = fname.find_last_of("\\/"); + const std::string curr_dir = (std::string::npos == pos) ? "" : fname.substr(0, pos); + AsciiFileStream stream{curr_dir + "/res/multiples_graph_1024.txt", false}; + node_id_t num_vertices = stream.vertices(); + + MinCutSketchAlg mc_alg(num_vertices, get_seed()); + + GraphSketchDriver driver(&mc_alg, &stream, driver_config); + driver.process_stream_until(END_OF_STREAM); + driver.prep_query(MINIMUMCUT); + driver.check_verifier(GraphVerifier(num_vertices, curr_dir + "/res/multiples_graph_1024.txt")); + + ASSERT_EQ(0, mc_alg.calc_minimum_cut().value); +} + +TEST(MinCutTest, CompleteGraphMinCut) { + auto driver_config = DriverConfiguration().worker_threads(8); + double epsilon = 0.8; + generate_stream(get_seed(), 1 << 13, 1, "./complete_stream.bin"); + size_t true_mc = (1 << 13) - 1; + + BinaryFileStream stream{"./complete_stream.bin"}; + node_id_t num_vertices = stream.vertices(); + + MinCutSketchAlg mc_alg(num_vertices, get_seed(), MCAlgConfiguration().epsilon(0.8)); + + GraphSketchDriver driver(&mc_alg, &stream, driver_config); + driver.process_stream_until(END_OF_STREAM); + driver.prep_query(MINIMUMCUT); + + MinCut ret = mc_alg.calc_minimum_cut(); + + ASSERT_GT(true_mc, ret.value); + ASSERT_LT(true_mc * (1 - epsilon), ret.value); + std::remove("./complete_stream.bin"); +} + +TEST(MinCutTest, DynamicMinCut) { + auto driver_config = DriverConfiguration().worker_threads(8); + double epsilon = 0.8; + node_id_t num_vertices = 1 << 13; + { + DynamicErdosGenerator gen(get_seed(), num_vertices, 0.5, 0.25, 0.25, 2); + gen.to_binary_file("./dynamic_stream.bin"); + } + + size_t true_mc = num_vertices / 2; // probably about half of complete graph min-cut + + BinaryFileStream stream{"./dynamic_stream.bin"}; + + MinCutSketchAlg mc_alg(num_vertices, get_seed(), MCAlgConfiguration().epsilon(0.8)); + + GraphSketchDriver driver(&mc_alg, &stream, driver_config); + driver.process_stream_until(END_OF_STREAM); + driver.prep_query(MINIMUMCUT); + + MinCut ret = mc_alg.calc_minimum_cut(); + + ASSERT_GT(true_mc, ret.value); + ASSERT_LT(true_mc * (1 - epsilon), ret.value); + std::remove("./dynamic_stream.bin"); +} + diff --git a/test/sketch_test.cpp b/test/sketch_test.cpp index e35f4aa2..004b740a 100644 --- a/test/sketch_test.cpp +++ b/test/sketch_test.cpp @@ -35,7 +35,7 @@ TEST(SketchTestSuite, TestSampleResults) { continue; } - col_hash_t depth = Bucket_Boruvka::get_index_depth(k, sketch2.column_seed(i), guesses); + col_hash_t depth = SketchBucket::get_index_depths(k, sketch2.column_seed(i - (i % 2)), guesses)[i & 0x1]; if (depth >= 2) { vec_idx[k] = false; // force all updates to only touch depths <= 1 i = 0; @@ -63,6 +63,37 @@ TEST(SketchTestSuite, TestSampleResults) { ASSERT_EQ(sketch2.sample().result, FAIL); } +TEST(SketchTestSuite, TestSketchSizeEstimate) { + size_t n = 4096; + size_t s = 26; + size_t dense_est = DenseSketch::estimate_bytes(n, s); + size_t sparse_est = SparseSketch::estimate_bytes(n, s); + + DenseSketch ds(n, 0, s); + SparseSketch ss(n, 0, s); + + ASSERT_EQ(ds.bucket_array_bytes(), dense_est); + ASSERT_EQ(ss.bucket_array_bytes(), sparse_est); +} + +TEST(SketchTestSuite, SparseDenseEquivalence) { + size_t n = 1 << 16; + size_t u = 1 << 13; + size_t samples = 4; + size_t seed = get_seed(); + + DenseSketch dense(n, seed, samples); + SparseSketch sparse(n, seed, samples); + + // perform some updates to both sketches + for (vec_t i = 0; i < u; i++) { + dense.update(i); + sparse.update(i); + } + + ASSERT_EQ(sparse, dense); +} + TEST(SketchTestSuite, GIVENonlyIndexZeroUpdatedTHENitWorks) { // GIVEN only the index 0 is updated Sketch sketch(40, get_seed(), 1, num_columns); @@ -102,7 +133,6 @@ void test_sketch_sample(unsigned long num_sketches, SampleResult ret_code = query_ret.result; if (ret_code == GOOD) { - //Multiple queries shouldn't happen, but if we do get here fail test ASSERT_LT(res_idx, vec_size) << "Sampled index out of bounds"; if (!test_vec.get_entry(res_idx)) { //Undetected sample error @@ -168,6 +198,7 @@ void test_sketch_merge(unsigned long num_sketches, sketch2.update(test_vec2.get_update(j)); } sketch1.merge(sketch2); + Sketch backup(sketch1); try { SketchSample query_ret = sketch1.sample(); vec_t res_idx = query_ret.idx; @@ -177,6 +208,11 @@ void test_sketch_merge(unsigned long num_sketches, ASSERT_LT(res_idx, vec_size) << "Sampled index out of bounds"; if (test_vec1.get_entry(res_idx) == test_vec2.get_entry(res_idx)) { sample_incorrect_failures++; + std::cerr << "GOT A SAMPLE INCORRECT ERROR!" << std::endl; + std::cerr << "Got: " << res_idx << std::endl; + std::cerr << sketch1 << std::endl; + std::cerr << backup << std::endl; + std::cerr << sketch2 << std::endl; } } else if (ret_code == ZERO) { @@ -189,6 +225,7 @@ void test_sketch_merge(unsigned long num_sketches, } if (!vec_zero) { sample_incorrect_failures++; + std::cout << "GOT INCORRECT ZERO!" << std::endl; } } else { // sketch failed @@ -209,26 +246,57 @@ void test_sketch_merge(unsigned long num_sketches, } TEST(SketchTestSuite, TestSketchMerge) { - test_sketch_merge(10000, 1e2, 100, 0.001, 0.03); - test_sketch_merge(1000, 1e3, 1000, 0.001, 0.03); - test_sketch_merge(1000, 1e4, 10000, 0.001, 0.03); + test_sketch_merge(10000, 1e2, 100, 0, 0.03); + test_sketch_merge(1000, 1e3, 1000, 0, 0.03); + test_sketch_merge(1000, 1e4, 10000, 0, 0.03); } TEST(SketchTestSuite, TestSketchRangeMerge) { - Sketch skt1(2048, get_seed(), 10, 3); - Sketch skt2(2048, get_seed(), 10, 3); + size_t seed = get_seed(); + Sketch skt1(2048, seed, 10, 3); + Sketch skt2(2048, seed, 10, 3); + Sketch temp_skt(2048, seed, 10, 3); + + for (vec_t i = 0; i < 1024; i++) { + skt1.update(i); + skt2.update(i + 256); + } + // allowed return values after merging are [0, 255] and [1024, 1279] + vec_t good_1 = 255; + vec_t good_2 = 1024; + vec_t good_3 = good_2 + 255; - skt1.sample(); - skt1.range_merge(skt2, 1, 1); + temp_skt.merge(skt1); - skt1.sample(); + skt1.range_merge(skt2, 0, 1); + SketchSample sample = skt1.sample(); + if (sample.result == GOOD) { + ASSERT_TRUE(sample.idx <= good_1 || (sample.idx >= good_2 && sample.idx <= good_3)); + } + skt1.zero_contents(); + skt1.merge(temp_skt); + + skt1.range_merge(skt2, 1, 1); + sample = skt1.sample(); + if (sample.result == GOOD) { + ASSERT_TRUE(sample.idx <= good_1 || (sample.idx >= good_2 && sample.idx <= good_3)); + } + skt1.zero_contents(); + skt1.merge(temp_skt); + skt1.range_merge(skt2, 2, 1); - - skt1.sample(); + sample = skt1.sample(); + if (sample.result == GOOD) { + ASSERT_TRUE(sample.idx <= good_1 || (sample.idx >= good_2 && sample.idx <= good_3)); + } + skt1.zero_contents(); + skt1.merge(temp_skt); + skt1.range_merge(skt2, 3, 1); - - skt1.sample(); - skt1.range_merge(skt2, 4, 1); + sample = skt1.sample(); + if (sample.result == GOOD) { + ASSERT_TRUE(sample.idx <= good_1 || (sample.idx >= good_2 && sample.idx <= good_3)); + } } /** @@ -308,7 +376,7 @@ TEST(SketchTestSuite, TestSerialization) { file.close(); auto in_file = std::fstream("./out_sketch.txt", std::ios::in | std::ios::binary); - Sketch reheated(vec_size, seed, in_file, 3, num_columns); + Sketch reheated(vec_size, seed, in_file, sketch.get_buckets(), 3, num_columns); ASSERT_EQ(sketch, reheated); } @@ -348,16 +416,11 @@ TEST(SketchTestSuite, TestExhaustiveQuery) { ASSERT_EQ(query_ret.idxs.size(), 0) << query_ret.result; } - // assert everything returned is valid and <= 10 things - ASSERT_LE(query_ret.idxs.size(), 10); + // assert everything returned is valid for (vec_t non_zero : query_ret.idxs) { ASSERT_GT(non_zero, 0); ASSERT_LE(non_zero, 10); } - - // assert everything returned is unique - std::set unique_elms(query_ret.idxs.begin(), query_ret.idxs.end()); - ASSERT_EQ(unique_elms.size(), query_ret.idxs.size()); } } @@ -433,7 +496,7 @@ TEST(SketchTestSuite, TestRawBucketUpdate) { const Bucket *data = sk1.get_readonly_bucket_ptr(); - sk2.merge_raw_bucket_buffer(data); + sk2.merge_raw_bucket_buffer(data, sk1.get_buckets()); SketchSample sample = sk2.sample(); @@ -446,7 +509,7 @@ TEST(SketchTestSuite, TestRawBucketUpdate) { Bucket *copy_data = new Bucket[sk1.get_buckets()]; memcpy(copy_data, data, sk1.bucket_array_bytes()); - sk2.merge_raw_bucket_buffer(copy_data); + sk2.merge_raw_bucket_buffer(copy_data, sk1.get_buckets()); sk2.reset_sample_state(); sample = sk2.sample(); diff --git a/test/util/graph_verifier.cpp b/test/util/graph_verifier.cpp index 4d35ec1d..3eb7a527 100644 --- a/test/util/graph_verifier.cpp +++ b/test/util/graph_verifier.cpp @@ -93,6 +93,7 @@ void GraphVerifier::verify_connected_components(const ConnectedComponents &cc) { // first check that the number of components is the same for both if (kruskal_ccs != cc.size()) { + std::cout << "expect: " << kruskal_ccs << ", got = " << cc.size() << std::endl; throw IncorrectCCException("Incorrect number of components!"); } diff --git a/tools/benchmark/BENCH.md b/tools/benchmark/BENCH.md index 7e233d8a..f428094e 100644 --- a/tools/benchmark/BENCH.md +++ b/tools/benchmark/BENCH.md @@ -14,7 +14,7 @@ Finally, `UserCounters` gives performance information unique to each benchmark. ## Benchmarks ### Hashing -Measures the performance of a variety of hashing methods against the current method used by `Bucket_Boruvka`. +Measures the performance of a variety of hashing methods against the current method used by `SketchBucket`. All methods hash a single 64 bit input 8 times using different hash seeds. Example output: diff --git a/tools/benchmark/graphcc_bench.cpp b/tools/benchmark/graphcc_bench.cpp index fc5b8995..af0c698c 100644 --- a/tools/benchmark/graphcc_bench.cpp +++ b/tools/benchmark/graphcc_bench.cpp @@ -170,7 +170,7 @@ static void BM_index_depth_hash(benchmark::State& state) { uint64_t input = 100'000; for (auto _ : state) { ++input; - benchmark::DoNotOptimize(Bucket_Boruvka::get_index_depth(input, seed, 20)); + benchmark::DoNotOptimize(SketchBucket::get_index_depth(input, seed, 20)); } state.counters["Hash Rate"] = benchmark::Counter(state.iterations(), benchmark::Counter::kIsRate); } @@ -180,7 +180,7 @@ static void BM_index_hash(benchmark::State& state) { uint64_t input = 100'000; for (auto _ : state) { ++input; - benchmark::DoNotOptimize(Bucket_Boruvka::get_index_hash(input, seed)); + benchmark::DoNotOptimize(SketchBucket::get_index_hash(input, seed)); } state.counters["Hash Rate"] = benchmark::Counter(state.iterations(), benchmark::Counter::kIsRate); } @@ -194,7 +194,7 @@ static void BM_update_bucket(benchmark::State& state) { for (auto _ : state) { ++input; ++checksum; - Bucket_Boruvka::update(bkt, input, checksum); + SketchBucket::update(bkt, input, checksum); benchmark::DoNotOptimize(bkt); } } @@ -265,7 +265,7 @@ static void BM_Sketch_Merge(benchmark::State& state) { s1.merge(s2); } } -BENCHMARK(BM_Sketch_Merge)->RangeMultiplier(10)->Range(1e3, 1e6); +BENCHMARK(BM_Sketch_Merge)->RangeMultiplier(4)->Range(KB << 4, MB << 4); static void BM_Sketch_Serialize(benchmark::State& state) { size_t n = state.range(0); diff --git a/tools/minimum_cut.cpp b/tools/minimum_cut.cpp new file mode 100644 index 00000000..b677db75 --- /dev/null +++ b/tools/minimum_cut.cpp @@ -0,0 +1,146 @@ +#include "min_cut_sketch_alg.h" +#include "graph_sketch_driver.h" +#include +#include +#include // for rusage + +static bool shutdown = false; + +static double get_max_mem_used() { + struct rusage data; + getrusage(RUSAGE_SELF, &data); + return (double) data.ru_maxrss / 1024.0; +} + +static size_t get_seed() { + auto now = std::chrono::high_resolution_clock::now(); + return std::chrono::duration_cast(now.time_since_epoch()).count(); +} + +/* + * Function which is run in a seperate thread and will query + * the graph for the number of updates it has processed + * @param total the total number of edge updates + * @param g the graph object to query + * @param start_time the time that we started stream ingestion + */ +static void track_insertions(uint64_t total, GraphSketchDriver *driver, + std::chrono::steady_clock::time_point start_time) { + total = total * 2; // we insert 2 edge updates per edge + + printf("Insertions\n"); + printf("Progress: | 0%%\r"); fflush(stdout); + std::chrono::steady_clock::time_point start = start_time; + std::chrono::steady_clock::time_point prev = start_time; + uint64_t prev_updates = 0; + + while(true) { + sleep(1); + uint64_t updates = driver->get_total_updates(); + std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); + std::chrono::duration total_diff = now - start; + std::chrono::duration cur_diff = now - prev; + + // calculate the insertion rate + uint64_t upd_delta = updates - prev_updates; + // divide insertions per second by 2 because each edge is split into two updates + // we care about edges per second not about stream updates + size_t ins_per_sec = (((double)(upd_delta)) / cur_diff.count()) / 2; + + if (updates >= total || shutdown) + break; + + // display the progress + int progress = updates / (total * .05); + printf("Progress:%s%s", std::string(progress, '=').c_str(), std::string(20 - progress, ' ').c_str()); + printf("| %i%% -- %lu per second\r", progress * 5, ins_per_sec); fflush(stdout); + } + + printf("Progress:====================| Done \n"); + return; +} + +int main(int argc, char **argv) { + if (argc != 5) { + std::cerr << "ERROR: Incorrect number of arguments!" << std::endl; + std::cerr << "Arguments: stream_file, num_queries, graph_workers, reader_threads" << std::endl; + exit(EXIT_FAILURE); + } + + shutdown = false; + std::string stream_file = argv[1]; + int num_queries = std::atoi(argv[2]); + if (num_queries < 1 || num_queries > 1000) { + std::cerr << "ERROR: Invalid number of queries! Must be > 0 and <= 1000" << std::endl; + exit(EXIT_FAILURE); + } + int num_threads = std::atoi(argv[3]); + if (num_threads < 1) { + std::cerr << "ERROR: Invalid number of graph workers! Must be > 0." << std::endl; + exit(EXIT_FAILURE); + } + size_t reader_threads = std::atol(argv[4]); + + double query_percent = 1.0 / num_queries; + size_t queries_in_stream = num_queries - 1; + + BinaryFileStream stream(stream_file); + node_id_t num_nodes = stream.vertices(); + size_t num_updates = stream.edges(); + std::cout << "Processing stream: " << stream_file << std::endl; + std::cout << "nodes = " << num_nodes << std::endl; + std::cout << "num_updates = " << num_updates << std::endl; + std::cout << std::endl; + + auto driver_config = DriverConfiguration() + .gutter_sys(CACHETREE) + .worker_threads(num_threads) + .stream_threads(reader_threads); + auto mc_config = MCAlgConfiguration().batch_factor(1.0); + MinCutSketchAlg mc_alg{num_nodes, get_seed(), mc_config}; + GraphSketchDriver driver{&mc_alg, &stream, driver_config}; + + auto ins_start = std::chrono::steady_clock::now(); + std::thread querier(track_insertions, num_updates, &driver, ins_start); + + for (size_t q = 0; q < queries_in_stream; q++) { + driver.process_stream_until((q+1) * query_percent * num_updates); + auto query_start = std::chrono::steady_clock::now(); + driver.prep_query(CONNECTIVITY); + auto mc = mc_alg.calc_minimum_cut(); + std::chrono::duration mc_time = std::chrono::steady_clock::now() - query_start; + std::chrono::duration flush_time = driver.flush_end - driver.flush_start; + + std::cout << "Query " << q + 1 << std::endl; + std::cout << "Total Minimum cut latency: " << mc_time.count() << std::endl; + std::cout << " Flush Gutters(sec): " << flush_time.count() << std::endl; + std::cout << " Creating SFs(sec): " << mc_alg.sf_total_duration.count() << std::endl; + std::cout << " Viecut algorithm(sec): " << mc_alg.viecut_duration.count() << std::endl; + std::cout << "Minimum Cut: " << mc.value << std::endl; + + } + + // finish the stream + driver.process_stream_until(END_OF_STREAM); + + auto query_start = std::chrono::steady_clock::now(); + driver.prep_query(CONNECTIVITY); + auto mc = mc_alg.calc_minimum_cut(); + std::chrono::duration mc_time = std::chrono::steady_clock::now() - query_start; + std::chrono::duration flush_time = driver.flush_end - driver.flush_start; + + std::chrono::duration insert_time = driver.flush_end - ins_start; + shutdown = true; + querier.join(); + + double num_seconds = insert_time.count(); + + std::cout << "Total insertion time(sec): " << num_seconds << std::endl; + std::cout << "Updates per second: " << stream.edges() / num_seconds << std::endl; + std::cout << "Total Minimum cut latency: " << mc_time.count() << std::endl; + std::cout << " Flush Gutters(sec): " << flush_time.count() << std::endl; + std::cout << " Creating SFs(sec): " << mc_alg.sf_total_duration.count() << std::endl; + std::cout << " Viecut algorithm(sec): " << mc_alg.viecut_duration.count() << std::endl; + std::cout << "Minimum Cut: " << mc.value << std::endl; + std::cout << "Maximum Memory Usage(MiB): " << get_max_mem_used() << std::endl; +} diff --git a/tools/process_stream.cpp b/tools/process_stream.cpp index 9e30b07e..56c1fd15 100644 --- a/tools/process_stream.cpp +++ b/tools/process_stream.cpp @@ -92,10 +92,14 @@ int main(int argc, char **argv) { std::cout << "num_updates = " << num_updates << std::endl; std::cout << std::endl; - auto driver_config = DriverConfiguration().gutter_sys(CACHETREE).worker_threads(num_threads); - auto cc_config = CCAlgConfiguration().batch_factor(1.0); + auto driver_config = DriverConfiguration() + .gutter_sys(CACHETREE) + .worker_threads(num_threads) + .stream_threads(reader_threads); + driver_config.gutter_conf().wq_batch_per_elm(4); + auto cc_config = CCAlgConfiguration().batch_factor(1); CCSketchAlg cc_alg{num_nodes, get_seed(), cc_config}; - GraphSketchDriver driver{&cc_alg, &stream, driver_config, reader_threads}; + GraphSketchDriver driver{&cc_alg, &stream, driver_config}; auto ins_start = std::chrono::steady_clock::now(); std::thread querier(track_insertions, num_updates, &driver, ins_start);