Repository navigation
Releases: ExaDev/pg_warren
Release list
pg_warren 0.8.0
Upgrading
- Run
ALTER EXTENSION pg_warren UPDATE;in every database that has
the extension, after installing the new build. The update replaces
the PL/pgSQL definitions ofwarren.pagerankandwarren.leidenwith
native ones (under Performance), keeping their privileges; it changes
no table. Until the update, 0.7.0's definitions still run with the new
library, as they did.
Behaviour changes
warren.pagerankrefuses a NULLoutordampingbefore it runs, as
out must not be NULLanddamping must not be NULL(SQLSTATE 22004,
as before; 0.7.0 ran first and then namedy, orvalue). An empty
or unwritableoutis refused before the run, too. Its intermediates
(_warren_pagerank_*) no longer exist, even during a run, and it no
longer carriesSET search_path: the access guard pins it for the call
(DESIGN.md §7). Everything else is kept: its signature, ranks to the
bit, iteration counts, SQLSTATEs, progress, parallel label and
privileges, including a NULLtolnever stopping it early and a NULL
max_iterrunning no iteration.leidenchecks that the caller may writeoutbefore it runs, not
after. The error is the same (42501). The function no longer sets its
ownsearch_path; like every otherwarren.*function, it pins it for
the call.- On graphs with fractional weights,
leidensums a level's edge weights
in a different order from 0.7.0'scontract. A weight may differ in its
last bit, and a tie between moves that it decides may then go the other
way. On unweighted and integer-weighted graphs the partitions, kept
levels and modularity are 0.7.0's, to the bit.
Fixes
gatheroverchar(n)messages no longer truncates them to one
character forcollectandfirst: their query was cast to
character, which ischaracter(1); it is now cast tobpchar, as
array_aggkeeps them.gather'scollect,firstandcountovertext,varcharor
charmessages no longer depend onclient_encoding: a message it
could not represent failed with 22P05 ("has no equivalent in
encoding"), since each message passed throughtextsend.
Performance
warren.pagerankis a native kernel, not a PL/pgSQL loop over the
compute primitives (DESIGN.md §6.7). The loop made eleven calls an
iteration, about 1.5 ms of overhead an iteration however small the
graph; the kernel makes two parallel passes and one product an
iteration, and holds four vectors of the id space (five with a seed)
where the loop held eleven: 0.32 GB of scratch at 10M nodes instead of
0.88 GB.
Twenty iterations, in a first single before-and-after run on a loaded
machine (median warm times,warren.threads = 4): lesmis 34.7 → 1.6 ms, facebook 47.3 → 6.3 ms,
email-enron 74.9 → 22.6 ms, cit-hepth 65.7 → 18.6 ms and DBLP
318 → 113 ms. The later three-round run timed lesmis at 3.1 ms.
Against igraph on the 12 head-to-head datasets (three
interleaved rounds,bench/results/corpus/head_to_head/README.md,
"PageRank in 0.8.0"): faster on 7 (roadNet-TX 0.22 s against 0.81 s,
DBLP 105 ms against 251 ms) and slower on 5 (WN18RR, cit-hepth and
YAGO3-10 by 1.2–1.8×, polblogs by 3.1×, and lesmis, 3.1 ms against
29 µs, by a call's fixed cost).leidenis native (DESIGN.md §6.7, "Leiden's levels"). Each level's
graph is contracted from the level below, not from the whole graph, and
is held in memory, charged towarren.max_compute_memory. Onlyout
and the levelslevels_prefixasks for are written. No level graph,
segment or WAL is written for a level, and the disk a run needed grew
with its levels before.- It finds the same partitions as before. A pg_test runs the frozen
0.7.0 function beside it. - On the head-to-head's real graphs it is 1.3–21× faster:
fb15k-237 1.44 → 0.21 s, yago3-10 2.26 → 0.73 s, roadnet-tx
7.9 → 3.8 s, lesmis 25 → 1.2 ms. - It takes 0.76–1.6× igraph's time on the medium tier, where it was
1.9–180×, and is faster than igraph on ss-gleich-usroads-48, dblp,
yago3-10 and roadnet-tx (bench/results/corpus/head_to_head/,
"Leiden in 0.8.0").
- It finds the same partitions as before. A pg_test runs the frozen
gatherovertext,varchar,charandbyteamessages keeps
them as their bytes, asconcatdid, instead of each going through its
type's send and receive functions, and lays a collected array out
straight from them: at 10M nodes and 11.5M messages, acollectof
cheap text messages takes 14.5 s rather than 16.7 s (13.5 s as
bigint). Appendix A.3's first superstep goes from 30.8 s to 29.5 s;
most of it is its message query (DESIGN.md §13, "After 0.7.0").distance('edit')across two graphs (graph2) no longer rebuilds
the second graph in the first's dense ids: it maps each edge as it
reads it and looks it up in the first graph's row (DESIGN.md §8). It
charges only the id mapping, 8 bytes a node of the second graph, where
it charged 96 bytes an edge for the rebuild, so Appendix A.2 at 10M
asks for 88 MB instead of 10.9 GB and runs under the 4GB default rather
than failing with 53400. At 2.2M nodes and 22M stored edges the call's
peak memory falls from 290 MB to 9 MB and its time from 1.33 s to
0.39 s. Results are unchanged.- A short kernel step no longer waits to be woken. The calling thread
spins for up to 50 µs before it parks while the compute pool runs a job,
so a job over a small graph returns in 2–4 µs instead of the 20–40 µs
the operating system took to wake the caller (DESIGN.md §6.9).
PageRank runs three jobs an iteration: on lesmis a call takes
1.3–1.4 ms instead of 3.5–3.7 ms (on a 3-node graph 1.0–1.2 ms instead
of 2.8–3.0 ms), and larger graphs are unchanged or slightly faster. - Measured: a traversal call's fixed cost is 25–70 µs, not 0.2–2 ms.
Warm, with the round trip,bfs,shortest_pathandwalktake
0.07–0.11 ms against 0.04 ms forSELECT 1, on PG 16 and 18. Half of
the server's share is the one catalog read that resolves the graph
(DESIGN.md §13, "After 0.7.0"). Nothing in traversal changed.
The apt repository's signing key has the fingerprint A7FFD766EE915EE8E97085F8DF33D7645F721A34;
check the key you fetched with gpg --show-keys /etc/apt/keyrings/pg_warren.asc.
pg_warren 0.7.0
Milestone M11 of DESIGN.md §13: proven on real graphs and in real
deployments. pg_warren now runs a suite of 19 real graphs pinned through
keepset (bench/corpus), checked against NetworkX and igraph and timed
head to head with igraph and Neo4j, with and without GDS (README, "How it
compares"), and Appendix A's use cases at 10M nodes. A pair of nodes
linked in several relations is now read one way everywhere: strengths
are summed and an unweighted pair counts once, costs take the pair's
best edge, and per_relation := true reads an edge in each relation; on
graphs with such pairs some results differ from 0.6.0's (under Behaviour
changes). A relation with few edges stores sparse offsets, so every graph
needs a refresh after the upgrade (under Upgrading). Concurrent
refreshes, crash recovery, streaming replicas, a connection pooler and a
SQL fuzzer found the bugs under Fixes, two of them server crashes.
Packages are published from this release on: a signed apt repository for
Debian and Ubuntu, Docker images, a Homebrew tap and tarballs, for amd64
and arm64 (under New).
Upgrading
- If
pg_warrenis inshared_preload_libraries, restart the server
after installing the new build: the postmaster keeps the library it
loaded at start, and every backend inherits it. - Restart the server if the shared cache is in use. The shared cache's
registry is nowpg_warren.cache.4, so backends of the old and the new
build never share a version; the old registry's segments are freed by a
restart. - Run
ALTER EXTENSION pg_warren UPDATE;in every database that has
the extension, after installing the new build and before using the
new version. The update drops and creates again the 27 functions that
gainper_relation(under New), keeping their privileges; it changes
no table. Until the update, 0.6.0's definitions still work with the new
library, reading a pair as the new default does, except 0.6.0's
sssp_vector(PL/pgSQL), which still sums a pair's weights until the
update. - The update drops those functions without
CASCADE, so it fails if a
view, or a function with aBEGIN ATOMICbody, calls one of them
("cannot drop function ... because other objects depend on it"). Drop
the dependent objects, runALTER EXTENSION pg_warren UPDATE, then
create them again. - Refresh every graph after installing the new build. A relation's
CSR can now store sparse offsets, so CSR segments have a new format
(2). Reading a version built by an earlier release fails with
object_not_in_prerequisite_state(55000) and a message naming
warren.refresh; arefreshbuilds on it as usual, and the version it
builds reads. Old versions are not converted. Vectors are bound to a
version, so recreate them on the new one.
Behaviour changes
-
An unweighted pair of nodes linked in several relations now counts
once inmodularity,contract,local_moving,leiden_refine,
louvainandleiden, as it already did inspmv,degreeand
pagerank. They counted it once per relation, so on graphs with such
pairs their results differ from 0.6.0's. A countingcontractcounts
the pair in the first of its relations. Weighted pairs are unchanged:
their weights are summed across relations. -
Functions that read edge weights as costs now take a weighted pair
linked in several relations at its best edge, not the sum of its
relations' weights:sssp_vector,betweenness_centrality,
closeness_centrality,harmonic_centralityand
minimum_spanning_forestits lightest relation's weight,
dag_longest_pathits heaviest. Strengths add, costs don't: a road of
5 km and a railway of 3 km between two towns are a route of 3 km, not
8. Through 0.6.0 these functions summed the weights, so on weighted
graphs with such pairs their results differ from 0.6.0's.dijkstra
already took the cheapest relation through 0.6.0, and still does.
Functions that read weights as strengths (spmv,spmm,pagerank
and the other spectral centralities, the community functions,
contract,eigen,distance) still sum them;spmvwithmin_plus
reads the sum too unless givenper_relation := true. Unweighted calls
are unchanged. See docs/semantics.md, "A pair in several relations". -
per_relation := truegives the cost functions the same result as
the default (dijkstra,sssp_vector, the three shortest-path
centralities,dag_longest_path,minimum_spanning_forest): as
parallel edges, a path takes the cheapest (longest paths the heaviest).
They accept it, and refuse NULL (22004), so thatper_relationmeans
one thing on every function. -
Undirected weighted centralities refuse a pair whose weight is NaN in
one direction (22023), as they refuse a NaN anywhere else. They used
the other direction's weight. -
eigen(anddistance('spectral')) returns every copy of the
eigenvalue each connected component gives: 0 oflaplacianand
normalized_laplacianwithwhich := 'smallest', 1 ofrandom_walk
withwhich := 'largest', and each bipartite component's −1 of
random_walkand 2 ofnormalized_laplacianwithwhich := 'largest'. Each copy is one component's eigenvector,
found exactly and without Lanczos steps, components in the order of
their lowest node, and Lanczos computes the rest of thekorthogonal
to them. Before, Lanczos found one copy and mistook the next eigenvalues
for the rest: 30 disjoint 40-cycles' three smallest Laplacian
eigenvalues came back as 0, 0.025 and 0.098, and on roadNet-TX (424
components) the call ran past a 10-minute timeout; four paths' random
walk gave 1, 1, 1, 1, −1, 0.9709, −0.9709 for its 7 largest, against
four −1s. A call all of whose pairs are the components' no longer warns
of several components; one with pairs from Lanczos still does. Other
results change in the last digits; a connected graph's 0 is now exactly
0. -
A graph's number of relations no longer refuses its build on its own.
warren.max_build_memoryused to be checked against 8 bytes per node
for every relation's CSR (and the merged CSR), in each direction; it is
now checked against the merged CSR's offsets and 8 bytes per relation,
and each relation's offsets count in the build's estimate by its edges. -
bfsexpands each depth's nodes in ascending dense id order (the
graph's internal node order), so within a depth its rows can come in a
different order than in 0.6.0, and a node reachable from several nodes
of the previous depth reports asviathe one of smallest dense id.
Nodes and depths are unchanged. -
walkandneighboursmake each row's external ids as the row is
returned, rather than all rows before the first, ascomponentsand
the others have since 0.6.0. Their rows and order are unchanged;walk
still runs the whole walk in its first call. Such a call keeps the
version it reads, private or mapped from the shared cache, until its
last row is read or it ends (under Performance).
New
-
per_relation boolean DEFAULT falseonmodularity,contract,
local_moving,leiden_refine,louvain,leiden,spmv,spmm,
pagerank,katz_centrality,eigenvector_centrality,hits,
sssp_vector,dijkstra,betweenness_centrality,
closeness_centrality,harmonic_centrality,dag_longest_path,
minimum_spanning_forest,degree,vector_degree,degree_vector,
aggregate_neighbours,gather,label_propagation_majority,eigen
anddistance:truereads a pair linked in several relations as an
edge in each (one per relation, as parallel edges), for when that
multiplicity is the signal. -
docs/limits.md, "Behind a connection pooler": what transaction
pooling loses and leaks between clients (settings, transient vectors,
last_stats(), SQLPREPARE) and what to use instead, checked by
test/pooler_soak.py, a soak behind PgBouncer in transaction and
session pooling that watches pooled backends, the shared cache and the
catalog for growth (CI runs it for 2 minutes on PG 16). -
Packages, published with each release from this one on (README.md,
"Install"): a signed apt repository ofpostgresql-NN-pg-warren
packages for PostgreSQL 15–18 on Debian 12 and 13 and Ubuntu 22.04 and
24.04, amd64 and arm64, built against PGDG's PostgreSQL; Docker images
on GHCR (ghcr.io/exadev/pg_warren:pgNNandX.Y.Z-pgNN, PGDG's
postgresimage with the package's files and pg_warren preloaded);
a Homebrew formula (brew install exadev/tap/pg_warren, for
postgresql@17and@18, bottled on Apple silicon); and tarballs for
other glibc Linux. Each GitHub Release has a signedSHA256SUMS. The
packages do not editpostgresql.conf: add the preload yourself. macOS
is not yet a supported platform (its tests run in CI without gating),
and RPM packages and PGXN are not built yet.
Fixes
- A read of a graph inside a parallel
CREATE TABLE AS(orCREATE MATERIALIZED VIEW,REFRESH MATERIALIZED VIEW) no longer fails with
"cannot start commands during a parallel operation". Such a statement
has a transaction id before its plan runs, so pgrx ran every catalog
read non-read-only, with a command counter increment, which Postgres
refuses in parallel mode once the command has used its command id: in
the leader as soon as it ran the function, and in a worker. In parallel
mode the catalog reads now run read-only, as cursors under the active
snapshot (DESIGN.md §6.9; found running Appendix A.3 at 10M nodes, whose
sampled oracle builds a table fromwarren.id_map; tests
every_read_runs_in_a_parallel_create_table_as,
a_parallel_create_table_as_runs_a_safe_read_in_its_leader_too). - **On a physical standby, every function that writes fails with
read_only_sql_transaction(25006), "cannot change pg_warren's catalog
in a read-only transaction"...