BE-782: Fit 2D semantic maps over graph entities and serve them as tiles - #9305
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
1 Skipped Deployment
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## bm/be-785-caller-identified-workflow-starts-and-owned-buffer-accessors #9305 +/- ##
=======================================================================================================
Coverage 60.76% 60.76%
=======================================================================================================
Files 1441 1441
Lines 143607 143607
Branches 6662 6662
=======================================================================================================
Hits 87261 87261
Misses 55255 55255
Partials 1091 1091
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Merging this PR will regress 2 benchmarks
Warning Please fix the performance issues or acknowledge them on CodSpeed. Performance Changes
Tip Investigate this regression by commenting Comparing |
7a73226 to
e488a6b
Compare
5270597 to
44b088f
Compare
TimDiekmann
left a comment
There was a problem hiding this comment.
Could you please give instructions on how to review this?
44b088f to
1b7962b
Compare
| "lint:clippy": "just clippy", | ||
| "start": "cargo run -p hash-graph-atlas --bin hash-graph-atlas --", | ||
| "test:integration": "cargo nextest run --package hash-graph-atlas --all-features --filterset 'kind(test)'", | ||
| "test:miri": "cargo miri nextest run -- vec2::tests::from_slice_reinterprets_in_place vec2::tests::natural_from_slice_splits_and_rejoins vec2::tests::layout_conversions_round_trip vec2::tests::simd_conversions_round_trip vec2::tests::from_lanes_inverts_lane_extraction vec2::tests::into_lanes_extracts_axis_groups vec2::tests::natural_from_lanes_interleaves vec2::tests::batch_getters_agree_across_layouts vec2::tests::natural_min_max_and_reductions bounds::tests::from_slice_matches_from_points bounds::tests::from_slice_rejects_non_finite dvec2::tests::dvec2x4t vecn::tests::boxed vecn::tests::lanes_ vecn::tests::vecn_wraps_in_place vecn::tests::vecn_wrap_slice dvecn::tests::wrapping_is_in_place vecn::tests::aligned_vecn_rejects dvecn::tests::aligned_accumulators dvecn::tests::aligned_from_mut dvecn::tests::aligned_from_ref dvecn::tests::boxed_dvecn_clone_from vecn::tests::try_as_aligned vecn::tests::from_slice_yields_every_row vecn::tests::from_slice_rejects_what matrixn::tests:: dataset::tests::archived_ dataset::auxiliary::tests:: random::tests::compat_ sprs::tests::unit_values adjacency::tests::writing_to_memory bitset::tests::dense_bit_slice_frames_round_trip bitset::tests::dense_bit_slice_frames_mutate_in_place bitset::tests::dense_bit_slice_zero_domain_frames_parse bitset::tests::dense_bit_slice_polices_the_final_word bitset::tests::dense_bit_slice_refuses_misshapen_frames bitset::tests::dense_bit_slice_zerocopy_doors_carry_the_frame_invariant bitset::tests::dense_bit_slice_prefix_reads_take_the_header_count bitset::tests::dense_bit_slice_array_starts_as_empty_frames bitset::tests::dense_bit_slice_array_indexes_independent_frames bitset::tests::dense_bit_slice_array_round_trips_through_its_region bitset::tests::dense_bit_slice_array_of_no_frames bitset::tests::dense_bit_slice_array_refuses_misshapen_regions bitset::tests::dense_bit_slice_array_zerocopy_doors_carry_the_region_invariant allocator::tests:: dsquare::tests::block_height_invariance dsquare::tests::the_solution_reproduces_the_right_hand_side dsquare::tests::both_dots_reduce_equal_inputs_to_identical_bits dsquare::tests::the_row_dot_and_dvecn_dot_reduce_equal_bytes_to_identical_bits dsquare::tests::every_row_is_aligned_for_simd dsquare::tests::writes_through_rows_land_at_their_offsets dsquare::tests::the_zero_matrix_reads_zero_everywhere dsquare::tests::the_empty_matrix_factors_and_solves dsquare::tests::a_row_past_the_order_panics dsquare::tests::a_mismatched_right_hand_side_panics dsquare::tests::matrix_drop_returns_the_buffer_to_its_allocator dsquare::tests::factor_drop_returns_the_moved_buffer_to_its_allocator runs::tests::structure_view scalar::tests::try_from_bytes morton::tests::try_from_ref authorization::tests::archived_actor_entity_uuid", |
There was a problem hiding this comment.
It would be good if this would be more maintainable. This way, it's very easy to miss.
| impl FromRequestParts<AppState> for Actor { | ||
| type Rejection = Problem<'static>; | ||
|
|
||
| fn from_request_parts( | ||
| parts: &mut Parts, | ||
| _state: &AppState, | ||
| ) -> impl Future<Output = Result<Self, Self::Rejection>> + Send { | ||
| future::ready(actor(parts).map(Self)) | ||
| } | ||
| } |
There was a problem hiding this comment.
This reinvents the existing Authentication setup, please use already existing authentication we have in hash-graph-api, so we don't have multiple locations where we define how we get the AuthenticatedActor.
The auth-setup itself lives in hash-graph-authentication. We may need to move some middleware from the api-crate to the authN-crate to re-use it here. This should reduce the amount of work being required.
There was a problem hiding this comment.
Yes, I understand, but I deliberately didn't want to depend on hash-graph-api since it doesn't make sense to do so just for the middleware. I tracked all of this in BE-805; I'm happy to do it as part of this PR or as a follow-up. Still, I didn't want to make this already massive PR bigger by moving things into, e.g. hash-graph-authentication, especially considering that before yesterday the way I was doing this was basically just 10 lines of code that were the same between them (which is no longer true), so it's just unfortunate timing^^. Several adjustments are needed here, including removing the hash-api proxy, moving to kratos, and moving the middleware into hash-graph-authentication, which I didn't want to do as part of this. I can prepare this under this PR and then adjust this PR, making it reviewable (for each), or you can take it up if you'd like. This should then also make the authentication wiring straightforward.
| //! The `X-Authenticated-User-Actor-Id` header names the actor, and that header is the trust | ||
| //! boundary the graph's REST API stands on. The gateway authenticates the session and states the |
| /// Parity with the graph's REST API, which accepts this name, fixes the spelling. Headers this | ||
| /// crate introduces carry no prefix. | ||
| const ACTOR_HEADER: &str = "X-Authenticated-User-Actor-Id"; |
1b7962b to
fa36c3c
Compare
| extract::FromRequestParts, | ||
| http::{StatusCode, request::Parts}, | ||
| }; | ||
| use hash_graph_authorization::policies::principal::actor::AuthenticatedActor; |
| extract::FromRequestParts, | ||
| http::{StatusCode, request::Parts}, | ||
| }; | ||
| use hash_graph_authorization::policies::principal::actor::AuthenticatedActor; |
| use hash_graph_authorization::policies::{ | ||
| MergePolicies, PolicyComponents, | ||
| action::ActionName, | ||
| principal::actor::AuthenticatedActor, |
| use hash_graph_authorization::policies::{ | ||
| MergePolicies, PolicyComponents, | ||
| action::ActionName, | ||
| principal::actor::AuthenticatedActor, |
| let policy_components = PolicyComponents::builder(store) | ||
| .with_actor(actor) |
| let temporal_axes = QueryTemporalAxesUnresolved::live_only().resolve(); | ||
| let mut compiler = SelectCompiler::new(Some(&temporal_axes), false); | ||
|
|
||
| let policy_components = PolicyComponents::builder(store) |
| let temporal_axes = QueryTemporalAxesUnresolved::live_only().resolve(); | ||
| let mut compiler = SelectCompiler::new(Some(&temporal_axes), false); | ||
|
|
||
| let policy_components = PolicyComponents::builder(store) |
| let mut compiler = SelectCompiler::new(Some(&temporal_axes), false); | ||
|
|
||
| let policy_components = PolicyComponents::builder(store) | ||
| .with_actor(actor) |
| *indices.last()? | ||
| }; | ||
|
|
||
| self.examples.remove(index); |
| }; | ||
| if single_group { | ||
| let index = *indices.last().expect("an example group is never empty"); | ||
| self.examples.remove(index); |
| } | ||
|
|
||
| for index in indices.iter().rev() { | ||
| self.examples.remove(*index); |
| .as_bytes() | ||
| .windows(8) | ||
| .any(|candidate| candidate == window), | ||
| "the rendering carries held material at position {start}: {rendered}" |
| "the rebuilt canonical frame does not reproduce the published coordinate column (max \ | ||
| component error {}, bound {CERTIFICATE_TOLERANCE})", | ||
| certificate.max_absolute_error, |
| .as_bytes() | ||
| .windows(8) | ||
| .any(|candidate| candidate == window), | ||
| "the rendering carries key material at position {start}: {rendered}" |
| // Redaction that hid the type would trade one diagnostic failure for another. | ||
| assert!( | ||
| rendered.contains("WireSecret"), | ||
| "the rendering still names the type: {rendered}" |
| let connection = DatabaseConnectionInfo::new( | ||
| DatabaseType::Postgres, | ||
| store("USER", "postgres"), | ||
| store("PASSWORD", "postgres"), |
Benchmark results
|
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 2002 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 1002 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: high, policies: 3314 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: medium, policies: 1527 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 2078 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 1033 | Flame Graph |
policy_resolution_medium
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 102 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 52 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: high, policies: 269 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: medium, policies: 108 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 133 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 63 | Flame Graph |
policy_resolution_none
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 2 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 2 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 8 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 3 | Flame Graph |
policy_resolution_small
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 52 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 26 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: high, policies: 94 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: medium, policies: 27 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 66 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 29 | Flame Graph |
read_scaling_complete
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_id;one_depth | 1 entities | Flame Graph | |
| entity_by_id;one_depth | 10 entities | Flame Graph | |
| entity_by_id;one_depth | 25 entities | Flame Graph | |
| entity_by_id;one_depth | 5 entities | Flame Graph | |
| entity_by_id;one_depth | 50 entities | Flame Graph | |
| entity_by_id;two_depth | 1 entities | Flame Graph | |
| entity_by_id;two_depth | 10 entities | Flame Graph | |
| entity_by_id;two_depth | 25 entities | Flame Graph | |
| entity_by_id;two_depth | 5 entities | Flame Graph | |
| entity_by_id;two_depth | 50 entities | Flame Graph | |
| entity_by_id;zero_depth | 1 entities | Flame Graph | |
| entity_by_id;zero_depth | 10 entities | Flame Graph | |
| entity_by_id;zero_depth | 25 entities | Flame Graph | |
| entity_by_id;zero_depth | 5 entities | Flame Graph | |
| entity_by_id;zero_depth | 50 entities | Flame Graph |
read_scaling_linkless
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_id | 1 entities | Flame Graph | |
| entity_by_id | 10 entities | Flame Graph | |
| entity_by_id | 100 entities | Flame Graph | |
| entity_by_id | 1000 entities | Flame Graph | |
| entity_by_id | 10000 entities | Flame Graph |
representative_read_entity
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/block/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/book/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/building/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/organization/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/page/v/2
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/person/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/playlist/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/song/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/uk-address/v/1
|
Flame Graph |
representative_read_entity_type
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| get_entity_type_by_id | Account ID: bf5a9ef5-dc3b-43cf-a291-6210c0321eba
|
Flame Graph |
representative_read_multiple_entities
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_property | traversal_paths=0 | 0 | |
| entity_by_property | traversal_paths=255 | 1,resolve_depths=inherit:1;values:255;properties:255;links:127;link_dests:126;type:true | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:0;link_dests:0;type:false | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:1;link_dests:0;type:true | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:2;links:1;link_dests:0;type:true | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:2;properties:2;links:1;link_dests:0;type:true | |
| link_by_source_by_property | traversal_paths=0 | 0 | |
| link_by_source_by_property | traversal_paths=255 | 1,resolve_depths=inherit:1;values:255;properties:255;links:127;link_dests:126;type:true | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:0;link_dests:0;type:false | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:1;link_dests:0;type:true | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:2;links:1;link_dests:0;type:true | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:2;properties:2;links:1;link_dests:0;type:true |
scenarios
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| full_test | query-limited | Flame Graph | |
| full_test | query-unlimited | Flame Graph | |
| linked_queries | query-limited | Flame Graph | |
| linked_queries | query-unlimited | Flame Graph |
🌟 What is the purpose of this PR?
The new
hash-graph-atlascrate fits 2D semantic maps over the entity embeddings stored in the graph, blending semantic similarity (what entities mean) with relational structure (how they connect), and distills each fitted map into a small encoder that places new entities on the existing map without refitting. Serving reads the fitted artifacts from disk and answers tile queries over them.The crate arrives whole and inert. Nothing links it until #9306 wires the CLI, the API proxy, and the compose service, so this layer reviews as a self-contained library plus its own
climodule.Review guidance for 627 files: line-reading is the wrong mode. The contract is at the boundaries, and they are small. The
climodule is the only public one (everything else ispub(crate)), the on-disk artifact formats live in thefilemodule with SHA-256 content identity inintegrity, and the wire format is documented inwire.md. The module docs top-down fromlib.rsgive the architecture: the SALT pipeline sits on domain-independent foundation modules (math,random,bitset,identity,morton,runs,progress,file).Review focus: determinism and cross-machine identity of the artifacts. x86-64 builds now assume the x86-64-v3 baseline (AVX2, FMA, BMI2) because the math kernels pin fused-multiply-add semantics, and the crate verifies the baseline at process entry so that a mismatched machine fails diagnosably. That
.cargo/config.tomlchange applies workspace-wide and is the one hunk here whose effect reaches beyond the crate.For an authorisation-focused review, one wants to take a look at (in order of building up on each other):
Atlas-Authoritytoken that we send over the wire to authenticateThe part to get right here is the cache semantics, as well as the cascading effect: we cannot backfill tiles, because that would trivially expose information about which tiles actually have occupancy. Everything revolves around that fact. The module enforcing it is
serve/schedule/: a restricted response delivers from its own first-occupant cascade over the visible rows alone, so a hidden row contributes to no output at all rather than leaving a hole.The fitting process (so everything in
salt/) is authorisation-blind. It only does Postgres queries (these are in thepostgres/folders respectively). The serve layer applies authorisation, supplied via the API layer.We can model it through the following diagram:
flowchart TD G["Gateway authenticates the session,<br/>states the actor in X-Authenticated-User-Actor-Id"] --> R{Route} R -->|manifest| P["Presented: reads the token if any, never rejects<br/>(api/authorization.rs)"] P --> MJ["Judge the generation, bind the filter,<br/>seal Scope: actor, filter digest, delivery cut<br/>XChaCha20-Poly1305 under a per-generation HKDF key"] MJ --> T[Atlas-Authority header back to the client] R -->|data route| AD["admit (api/authorization.rs)"] AD --> Q{"tag valid? actor matches the header?<br/>issued_at inside the window? epoch held?"} Q -->|any refusal| U["uniform 401, oracle-free:<br/>client re-fetches the manifest"] Q -->|ok| S["sealed Scope enters the visibility cache<br/>key = generation + actor + filter digest<br/>soft 8 min refresh-behind, hard 10 min ceiling<br/>(hard = revocation lag = token-age bound)"] S -->|hit| V[VisibilityProof] S -->|miss, one resolve per scope| C["compile policy + caller filter<br/>(serve/hydrate/compile.rs)"] C --> E{"admits_every_row?<br/>no caller filter AND policy filter = empty Filter::All"} E -->|yes| F["full_visibility: every row of every domain"] E -->|no| M["from_masks: fail-closed masks"] F --> V M --> V V --> W["assembly answers under the proof:<br/>every corpus-bearing response is masked"] W --> D["delivery from a per-scope schedule at cut k<br/>(serve/schedule/): built over visible rows alone,<br/>hidden rows contribute to nothing - no tile backfill"]The crate layout is roughly:
🔍 What does this change?
libs/@local/graph/atlas/: the crate (614 files), its docs, benches, and tests..cargo/config.toml: the x86-64-v3 baseline for x86-64 targets, workspace-wide..clippy.toml:core::fmt::LowerExpjoinsallow-renamed-params-for..dockerignore: excludeswire.md..github/actions/prune-repository/prune.py: learns the atlas edge (inert until the frontend merges).Cargo.toml,Cargo.lock,yarn.lock: crate and package registration.Pre-Merge Checklist 🚀
🚢 Has this modified a publishable library?
This PR:
📜 Does this require a change to the docs?
The changes in this PR:
🕸️ Does this require a change to the Turbo Graph?
The changes in this PR:
turbo.json's have been updated to reflect this🛡 What tests cover this?
cargo check, clippy at zero warnings across targets and features,fmtclean.❓ How to test this?
cargo nextest run -p hash-graph-atlas --lib cargo test -p hash-graph-atlas --doc