Sketch: triad_count (0.3)#
Status: reviewed sketch (M3, against the frozen 0.1 contract, ADR 0023); frozen in M9.
Computes: the 30-bin closed h-motif counts of a hypergraph (the triads of three hyperedges and
how they overlap), or the StatHyper type 1 / 2 / 3 counts, kept exact under hyperedge batches.
Paper: ESCHER (IPDPS 2026), ESCHER+ (TKDE 2026). Origin: ESCHER-GPU HMotifCount,
HMotifCountUpdate, type1/2/3, the wedge engine kernel/wedge.cuh (PLAN Section 6.4.6).
The header#
// cpp/include/dyng/triad_count.hpp
namespace dyng::triad_count {
enum class pattern : std::uint8_t {
h_motif, ///< the 30 closed h-motifs (bins 21-26, the open triads, are always 0)
stathyper_type1, ///< StatHyper's three triad types, one bin each
stathyper_type2,
stathyper_type3,
};
struct options { ///< an aggregate; fields are only appended
pattern kind = pattern::h_motif; ///< fixed at compute()
engine cuda_engine = engine::automatic; ///< fused: the wedge engine (Tier B). A tunable
};
struct stats : update_stats { ///< update_stats: affected = bins whose count changed
hyper_apply_summary batch; ///< (deterministic); iterations = 0; fallback_used false
std::int64_t deleted_frontier = 0; ///< deterministic: hyperedges of the subtract phase
std::int64_t inserted_frontier = 0; ///< deterministic: hyperedges of the add phase
std::uint64_t triads_removed = 0; ///< deterministic: summed over the bins
std::uint64_t triads_added = 0;
};
class result { ///< opaque, move-only (pimpl); a host array of counts
public:
[[nodiscard]] pattern kind() const noexcept;
[[nodiscard]] int num_bins() const noexcept; ///< 30 or 1
[[nodiscard]] array_view<const std::uint64_t> counts() const; ///< host memory on every backend
[[nodiscard]] std::uint64_t count(int bin) const;
[[nodiscard]] std::uint64_t total() const;
[[nodiscard]] const options& get_options() const;
void set_options(const options& opt); ///< tunables only
[[nodiscard]] std::uint64_t graph_version() const noexcept;
[[nodiscard]] memory_space space() const noexcept; ///< memory_space::host
[[nodiscard]] result clone(const resources& res) const;
};
template <typename vertex_t, typename weight_t>
[[nodiscard]] result compute(const resources& res, const hypergraph<vertex_t, weight_t>& hg,
const options& opt = {});
template <typename vertex_t, typename weight_t>
stats update(const resources& res, hypergraph<vertex_t, weight_t>& hg,
const hyperedge_batch_view<vertex_t, weight_t>& batch, result& r);
} // namespace dyng::triad_count
The update is cycle_count’s template on a hypergraph (aggregate delta, PLAN 4.5.1):
count(-)on the old h2h / h2v views with the deleted ids as the frontier, the commit, thencount(+)on the new views withhyper_apply_summary::inserted_idsas the frontier; each affected triad is owned by its smallest frontier member (ownership::min_member, invariant I2). The bins are signed 64-bit on the device and folded into the unsigned histogram infinalize(a bin that would become negative is aninternal_error, as in cycle_count).Stages:
triad_count.count_minus,triad_count.commit,triad_count.identify_affected,triad_count.count_plus,triad_count.finalize;triad_count.countforcompute()(the paper’s full count);hypergraph.applyinside the commit.Requirements: a line graph (
line_graph_policy::automaticmaterializessorted_merge) and the vertex-to-hyperedge rows (store_v2h).compute()checks them and names the property.Rule of two: the
countoperator withownership::min_memberis the second user of the aggregate-delta machinery after cycle_count; that is whenoperators::count_deltaenterscpp/src/operators/(PLAN 4.5.3).
Contract and registry#
Item |
Value |
|---|---|
Family |
aggregate delta |
Oracle |
|
Determinism |
|
Container requirements |
|
Backends |
sequential and OpenMP (the CPU reference |
|
strong before the commit; basic after it (result poisoned) |
Review against the 0.1 contract#
Check |
Result |
|---|---|
Uniform skeleton |
yes; |
Frozen headers needed |
|
Differences from PLAN 5.3 / 5.5 |
the plan’s |
Python |
|
Open questions (decided in M9)#
Whether the per-bin signed delta is worth a
statsfield after all (the paper’s plots show per-batch deltas; a fixed-sizestd::array<std::int64_t, 30>would not allocate).The
experimental::count_local_patterns<op_t>extension point (0.5) and how its Op concept relates topattern(a new pattern as a new Op, not a new enumerator).