Skip to content

Object id PR 1a: no home PE in the id, 49-bit packed index, hashed ids with per-process tranches, delivery by id - #4017

Merged
ritvikrao merged 9 commits into
objid-redesignfrom
objid-pr1a-layout
Oct 9, 2026
Merged

ritvikrao merged 9 commits into
objid-redesignfrom
objid-pr1a-layout

Conversation

@lvkale

@lvkale lvkale commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

PR 1a of the object id redesign (#3994), stacked on objid-redesign; design in doc/objid64-design.md sections 1-6 on that branch. Merges into the integration branch, not the reviewed line.

What changes

  • Layout: tag 3 | collection 12 (build-time CMK_OBJID_COLLECTION_BITS) | payload 49. CMK_OBJID_HOME_BITS is gone; no PE number lives in the id. Bounded arrays pack the index into the whole payload (16 bits before). Unbounded arrays get a hash key of the index plus a number unique within the array; the key width is set per run from the process count and +objid_expand (default 8) and travels with every checkpoint.
  • One home per element: homePe(idx) and homePe(id) agree by construction (map for packed arrays; index hash to a process for hashed arrays). The cache gets its manager back-pointer to compute it.
  • Minting: hashed ids come synchronously from a per-process tranche with one atomic cursor; the per-PE counter and its silent overflow are gone. Refills (the allocator) are PR 1b; until then exhausting the initial tranche aborts with a message.
  • Delivery by id: receive-side misses go to homePe(id) via handleUnknownByID (Aditya's e417584 folded in); lookupIdx loses its linear scan; requestLocationOnce stops repeated detours; the "two hops then home" rule is enabled.
  • LB global update now carries the element's true epoch from the emigrating PE.
  • Restart: ids never change; directories are rebuilt (restore() informs the home; the __FAULT__ cache pup is dropped). Restarting with a different number of processes aborts for hashed arrays until PR 1b; packed arrays restart on any count.
  • New test tests/charm++/unbounded_restart.

Tested on reconverse-darwin-arm8 (production), 1-3 processes: pingpong, megatest, hello/4darray, startupTest, anytime_bcastred, unbounded_migration, the three load_balancing tests with RotateLB, chkpt (same count; 4->2 and 2->4 PEs), unbounded_restart, and a 200,000-element unbounded insert from one PE. Found on the way, not caused here (reproduced on the unmodified build): a 2-process x 2-PE checkpoint restart hangs after "CkRestartMain done"; TreeLB asserts when a program calls migrateMe under an active balancer (program error).

Confidence tests and fixes added 2026-09-30

Five test programs, each run single-process and under lcrun with 2 and 4 processes on macOS (in a single process every hashed-kind home is PE 0, so the multi-process runs are the ones that exercise hashed-kind homes):

  • tests/charm++/objid_home: from every PE, homePe(idx) == homePe(id), packed ids computable on PEs that never saw the index, hashed ids obey the key/unique split and land in the creating process's tranche, ids and homes unchanged after two migrations.
  • tests/charm++/objid_insert (both kinds): insertion from every PE, synchronous [inline] local insertion, cold sends by index, sends over one- and two-hop-stale caches, [createhome] demand creation, ckDestroy.
  • tests/charm++/anytime_bcastred -u: the existing migration stress on an unbounded array; the reconverse stress job runs it too.
  • tests/charm++/objid_sections: ckmulticast sections on a hashed-kind array, migrating every round.
  • tests/charm++/objid_bound: bound arrays over a hashed-kind array under migrateMe and under +balancer RotateLB/GreedyLB, including a never-inserted bound array reached only through [createhome] (deliverInline's bound-sibling demand creation, the last user of the reverse lookup).

Fixes they found, each its own commit:

  • 6d0029d: the "after two hops go home" rule in recvMsg redirected to the home on the home PE itself, so the home re-sent the message to itself forever (this PR's regression; failed anytime_bcastred in CI).
  • d7a76ab (pre-existing): a [createhome] request from a PE with no id never got a reply, so the requester's buffered messages were never delivered. bufferForCreation now also requests the location by index.
  • c2ce757 (pre-existing, upstream main too): deliverInline delivered to the null pointer looked up before demand-creating a bound sibling.

Not fixed, to file separately: a direct ckDestroy() inside a broadcast entry method segfaults in CkArrayBroadcaster::attemptDelivery (base runtime too); the tests use thisProxy[thisIndex].ckDestroy().

🤖 Generated with Claude Code

lvkale and others added 9 commits October 1, 2026 01:28
…ds with a per-process tranche; delivery by id

PR 1a of the object id redesign (#3994; design in doc/objid64-design.md on
this branch, sections 1-6). The layout, the home function and the delivery
path change here; the tranche allocator with refills and the per-process
directory follow in later changes.

Layout (objid.h). The id is 3 type tag bits, CMK_OBJID_COLLECTION_BITS
(default 12, build-time) and a 49-bit payload. CMK_OBJID_HOME_BITS is gone:
no PE number is stored in the id. An array with bounds that fit packs its
index into the whole payload (16 bits before). An array without a usable
compressor gets a "hashed" payload: a hash key of the index in the top bits
and a number unique within the array in the low bits. The key width is fixed
per run from the process count and the +objid_expand factor (default 8:
the presumed ceiling on later expansion), stored in ck::objid::Layout, one
copy per process, and written into every checkpoint with the readonlies so
a restart keeps the layout the ids were minted under.

One home per element (cklocation.h). CkLocMgr::homePe(idx) and homePe(id)
now agree by construction: a packed-index array uses its map, as before,
decoding the index from the id when needed; a hashed array hashes the index
to a process (key % CkNumNodes(), rank 0 of it) from the id or from the
index alike. The old CkLocMgr::homePe(id), which read home bits, and the
creator-PE home of counter-minted ids are gone, and with them the two homes
that disagreed for every dynamically inserted array. CkLocCache gets its
manager back-pointer so it can compute the home of an id.

Minting (cklocation.C). A hashed id is minted synchronously on the inserting
PE from its process's tranche of unique numbers: the lower half of the
unique space split evenly among the processes present at launch, with one
atomic cursor per process per array. Exhausting the initial tranche aborts
with a message; refills come with the allocator in a later change. The
per-PE idCounter and its silent overflow are gone. The index hash is a
splitmix64 mix of the index words, not CkArrayIndex::hash().

Delivery (ckarray.C). A receive-side cache miss forwards to homePe(id)
through handleUnknownByID (Aditya Bhosale's e417584, folded in): the index
is never recovered off the source PE, so lookupIdx loses its linear scan of
idx2id and aborts if asked about an element this PE has never seen. A sender
that forwards via the home now also asks for the location once
(requestLocationOnce), so later sends do not repeat the detour. The
disabled "after two hops, send home" rule is enabled: sound now that the
home of an id is the home of its index.

Load balancer global update. Under CMK_GLOBAL_LOCATION_UPDATE the emigrating
PE broadcasts the new location with the element's true epoch, in place of
UpdateLocation fabricating one from each receiver's stale cache (which let
an older reply overwrite a newer location).

Restart. Ids never change: packed ids are functions of the index, hashed
ids are restored verbatim. Directories are rebuilt, not restored: restore()
now informs the home like resume() does, and the __FAULT__ branch of
CkLocCache::pup that shipped the home's remote entries is dropped. The
process's tranche cursor travels with rank 0's branch. Until the allocator
lands, restarting with a different number of processes aborts for hashed
arrays, with a message naming the array; packed arrays restart on any
process and PE count, and hashed arrays on any PE count.

New test tests/charm++/unbounded_restart: checkpoint an unbounded array,
restart on the same and on half the PEs, ping every element.

Tested on reconverse-darwin-arm8 (production), single process and with 2-3
processes: pingpong, megatest, hello/4darray, startupTest, anytime_bcastred,
unbounded_migration (also on 6 PEs across 3 processes), the three
load_balancing tests with RotateLB, chkpt (same count, and 4->2 and 2->4 PEs
in one process), unbounded_restart, and a 200,000-element unbounded insert
from one PE that the counter scheme aborted at 65,536. Not tested: a
2-process x 2-PE restart, which hangs on the unmodified branch as well.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
recvMsg's "after two hops go home" rule replaced the cached PE with the home PE
whenever the cached PE was not this PE. At the home itself that made the home
re-send the message to itself forever, so an element that had migrated twice
never received it (anytime_bcastred hung at step 3 in the reconverse stress
job, seed 1; every seed on the Mac). Apply the rule only on PEs other than the
home; the home follows its own entry.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
bufferForCreation buffered the message and sent requestDemandCreation to the
home, but for createhome the request named the home itself as the creation
PE, so after creating the element the home told nobody. The requester's
bufferedCreationMsgs entry was never flushed: only messages sent from the
home PE ever reached a demand-created element, on the pre-redesign runtime
too. The packed kind hid it because the source holds the id and forwards the
small message to the home instead of buffering.

Pair the request with a location request by index (new
CkLocMgr::requestLocationAtHome); the home buffers it until the element
exists and its reply flushes the buffered messages. Found by the new
tests/charm++/objid_insert test on the hashed kind.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Packed 3D array with 64^3 bounds (18 index bits, beyond the old 16-bit
budget) and a hashed 2D array. From every PE, for every element: homePe(idx)
== homePe(id); packed ids are computable on PEs that never saw the index;
hashed ids satisfy key == indexHashKey(idx) and home == rank 0 of key %
CkNumNodes(); all PEs agree on every home; ids are distinct and each hashed
unique part lies in its creating process's initial tranche. Repeated after
two migrations of every element: ids and homes unchanged.

The tranche-size check recomputes the file-local formula of cklocation.C
(initial region halved and split by process count); update it together.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…d creation, deletion

Runs in both kinds (-b for packed). Every PE inserts elements locally (and
checks ckLocal() is non-null on return) and on the next PE; every PE then
sends by index to every element, including ones it never touched; elements
migrate to random PEs and the sends repeat over one-hop and two-hop stale
caches (the recvMsg "more than one hop, go home" rule), five cycles; a
[createhome] entry sent from every PE to never-inserted indices must create
each element exactly once at its home; half the elements destroy themselves
and the sends repeat over the survivors.

Uses thisProxy[thisIndex].ckDestroy(): a direct ckDestroy() inside a
broadcast entry method segfaults in CkArrayBroadcaster::attemptDelivery on
the base runtime as well (to be filed separately).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…array

With -u the array is created without bounds and inserted dynamically, so
its element ids are the hashed kind (home = rank 0 of the process chosen by
the index hash) instead of the packed index. Behaviour without -u is
unchanged. The test targets and the reconverse stress job run both.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nder migration

Two overlapping sections of a 32-element unbounded array are built once;
for 30 rounds main multicasts to both, each member checks it was reached
through a section it belongs to and that its id is unchanged since
insertion, section reductions are checked, and then every element migrates
to a random PE. Section cookies and spanning trees hold element ids, which
the redesign keeps stable for the element's lifetime.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ale null pointer

deliverInline's bound-sibling branch demand-creates the missing element and
then delivered to the pointer looked up before the creation, which is still
null; CkAssert(elem) in deliverToElement is compiled out in production, so
the first message to a demand-created bound sibling dereferenced null (also on
upstream main and classic netlrts). Re-look the element up after creation and
abort with a message if it is still absent. Found by tests/charm++/objid_bound.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…anual and LB migration

A is unbounded (hashed ids); B is bound to A and inserted; C is bound to A,
never inserted, and reached only through a [createhome] entry, so its
elements are demand-created on the sibling's PE by deliverInline's
bound-sibling branch (the one remaining user of the reverse lookup). Each
round A[i] messages B[i] and C[i], which check they are co-located with A[i],
and B[i] messages A[i+1]; then A migrates (migrateMe, or AtSync with
+balancer RotateLB / GreedyLB) and B and C must follow: their migration
counts are enforced equal to A's. Runs single-process and under lcrun.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@lvkale
lvkale force-pushed the objid-pr1a-layout branch from 19d05e5 to dca0730 Compare October 1, 2026 06:28
@adityapb

adityapb commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

Review of this PR at dca0730 against doc/objid64-design.md. These come from reading the code; I haven't reproduced them yet. Most of the change matches the design and looks right. Two demand-creation regressions and the CMK_GLOBAL_LOCATION_UPDATE change should be fixed before merge.

Must fix

1. Demand creation is lost when the message reaches the home by id (regression for bounded arrays).
recvMsg now always calls handleUnknownByID (ckarray.C:1874). e417584 tried tryLookupIdx first and only routed by id when the index was unknown. handleUnknownByID has no demand-creation branch: at the home it parks the message in bufferedIDMsgs and calls requestLocation(id), which does nothing on the home.

  • Example: bounded 1D array with a [createhome] entry; element 5 never inserted; its home is PE 1. PE 0 sends arr[5].poke(). PE 0 has the id (packed index), so it forwards the message to PE 1 (every message counts as small at the default +messageBufferingThreshold). PE 1 parks it, and element 5 is never created. On objid-redesign, PE 1 recovers the index and creates the element (handleUnknown → bufferForCreation).
  • [createhere]: a forwarded createhere message that lands where the element is unknown is now parked. Before, the element was created on the sending PE.
  • objid_insert phase 4 misses this because every PE sends revive, including the home. The home's own send creates the element, which releases the parked messages. A variant where only non-home PEs send would catch it.
  • Suggested fix: in handleUnknownByID, when the message asks for creation and the index can be recovered (compressor, or a local record), go through handleUnknown(idx). Re-creating a deleted element of an unbounded array still needs an id → index lookup at the home, since the reverse scan is gone.

2. requestLocationAtHome (d37b78c) makes the home reply with an empty entry.
CkLocMgr::requestLocation(idx, peToTell) (cklocation.C:2842) replies whenever it finds an id for the index, even if the location is unknown. For bounded arrays the id is always found, so the reply is CkLocEntry::nullEntry (id 0, pe -1). Before this PR, bounded arrays never sent the index-keyed request, so this couldn't happen.

  • Example: bounded array; a [createhere] send from a non-home PE R to a missing element. The home tells R to create it, then answers R's location request with the null entry, because R hasn't informed the home yet.
  • Debug builds: CkAssert(e.pe != -1) in CkLocMgr::updateLocation aborts on R every time.
  • Production builds: the index listeners call sendBufferedMsgs(idx, 0, -1), so any messages R holds for element id 0 are sent to PE -1. Id 0 is a real element: index 0 of a bounded array, or the first hashed id on process 0.
  • Hashed kind: once an element has been deleted (reclaimRemote keeps the idx → id binding), the same reply overwrites R's idx2id[idx] with 0.
  • Not caught because testing used production builds and the new test only uses [createhome].
  • Suggested fix: reply only when cache->getPe(id) != -1. Otherwise push onto bufferedLocationRequests, which updateLocation already answers once the location is known.

3. CMK_GLOBAL_LOCATION_UPDATE: the update no longer happens before the program resumes.
UpdateLocation is now empty (cklocation.C:93) and emigrate broadcasts the new location instead (cklocation.C:3247). The comment in CentralLB (CentralLB.C:1049-1065, still present) explains that every PE, the destination included, must learn the new location before the move is acted on. Now the resume broadcast from PE 0 and each migration's broadcast from its source travel independently, so a PE can resume and send with a stale location.

  • GPU zerocopy: the receiver aborts with "Current PE does not match the destination PE..." (ckrdmadevice.C:259, also line 1230) on builds without CMK_GPU_COMM. That's the configuration the GPU line enables this flag for.
  • Debug builds: the extra hop hits CkAssert(false) in multiHop (cklocation.C:2964).
  • Cost: one group broadcast per migrated element, so migrations × PEs messages per LB step, where the old path sent none.
  • Design section 4.4 keeps UpdateLocation and makes it id-keyed, so this departs from the doc. The commit message lists no build with this flag.
  • Suggested fix: keep UpdateLocation synchronous and id-keyed. Either carry the element's true epoch in MigrateInfo from the source's LB stats, or keep the fabricated epoch for ordering and the broadcast to correct it.

Smaller

  • decompress uses (1 << bits) - 1 (ckarrayindex.h:426). With the 49-bit budget one dimension can now need 31 bits, which is undefined for a signed shift. ((CmiUInt8)1 << bits) - 1 matches what compress does with 1UL.
  • pupLayout is added to CkPupROData (ckcheckpoint.C:432) with no version marker, so restarting from a checkpoint written before this PR misreads _numReadonlyMsgs silently instead of failing clearly.
  • The restart abort at cklocation.C:2623 also fires when the process count is unchanged but packedByNode != CkMyNode() (e.g. 2×2 → 2×1 PEs, where PE 0's copy is restored everywhere). The message says "different process count", which is misleading there.
  • The uniqueBits < 16 check in initLayout (cklocation.C:2376) runs at every startup, whether or not a hashed array exists. With --ampi-only's CMK_OBJID_COLLECTION_BITS=29 (payload 32), every run above 8,192 processes would abort. Moot until AMPI is supported, but checking at the first hashed CkLocMgr would be safer.
  • pendingLocReqs entries stay stuck when the home drops the request (CkLocCache::requestLocation(id, pe) drops unknown ids). It mostly clears through multiHop, and section 4.3 says PR 1b removes requestLocationOnce anyway.

Looks right

  • Layout, masks and static_asserts.
  • homePe(idx) == homePe(id) by construction for both kinds; every homePe(id) caller passes the payload (array_element_id()), not the full ObjID.
  • restore() now informs the home, replacing the __FAULT__ cache pup.
  • Tranche cursor: lock-free fetch_add, and a restore only raises it.
  • cf394e6 (home no longer redirects to itself) and ba609ba (re-lookup after demand creation; a real null dereference on main too).

@ritvikrao
ritvikrao merged commit e6e52a9 into objid-redesign Oct 9, 2026
5 checks passed
lvkale added a commit that referenced this pull request Oct 9, 2026
Brings in #4016 (PR 0, squash-merged there; this branch had the original
commits), #4014 CkTreeCacheManager, #4020 trace-summary message bytes, and
the 2026-09-30 triage documents. The three conflicts were PR 0 text that
PR 1a (#4017) had already rewritten; the objid-redesign side is kept in
each, so the 1a files are unchanged by this merge.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@lvkale

lvkale commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

This merged on the 2026-10-01 approval, ahead of Aditya's 2026-10-04 review above. The merged head (dca0730) is exactly the commit that review examined, so its three must-fix items are now in objid-redesign unaddressed:

  1. demand creation lost when a message reaches the home by id (handleUnknownByID has no creation branch);
  2. requestLocation(idx, peToTell) replying with a null entry for bounded arrays;
  3. CMK_GLOBAL_LOCATION_UPDATE no longer synchronous before the program resumes.

A fix PR against objid-redesign follows, with a test for each item and the smaller points from the same review; Aditya's approval is required on it. Until it lands, please do not merge #4015 or #4021. objid-redesign is an integration branch with no users, so nothing on the reviewed line is affected.

lvkale added a commit that referenced this pull request Oct 10, 2026
…, synchronous LB location update

Addresses Aditya's review of #4017 (2026-10-04), which was merged into
objid-redesign before the review was acted on. The three must-fix items and
the smaller ones; design doc sections 3.4 and 4.4 record what was done.

1. Demand creation is lost when a message reaches the home by id.
   CkArray::handleUnknownByID parked every message in bufferedIDMsgs and asked
   for a location that nothing would ever report, because the element did not
   exist. For a message that asks for creation it now recovers the index and
   takes the index-keyed path (handleUnknown), which approves creation through
   the home for createhome and creates at the original sender for createhere.
   New CkLocMgr::recoverIndex(id, idx, scanAtHome): the compressor, a local
   record, or, for a hashed-kind element at its home, a linear scan of the
   home's idx -> id bindings (kept by reclaimRemote; the case is the
   re-creation of a deleted element, and no delivery path uses the scan). A
   non-home PE that cannot name the index hands the message to the home.

2. requestLocation(idx, peToTell) answered with the null entry.
   For a packed-index array the id of any in-bounds index is always known, so
   the home replied as soon as it was asked, with pe -1 and id 0, which
   asserts in updateLocation (debug builds) and in production sends buffered
   messages for id 0 to PE -1. The home now answers only when it knows the
   location and otherwise holds the request in bufferedLocationRequests,
   which updateLocation answers once the element registers.

3. CMK_GLOBAL_LOCATION_UPDATE: the update was no longer synchronous.
   UpdateLocation had been emptied in favour of a broadcast from the
   emigrating PE, so a PE could resume after a balancing step and address an
   element at its old location (an extra hop; GPU zerocopy aborts on it, and
   multiHop asserts in this mode). The synchronous update is back, keyed by
   id (CkLocMgr::updateLocationFromLB), with the epoch counted from the
   receiver's cache as before; that count never exceeds the element's true
   epoch, so the destination's insert still applies over it. The broadcast
   stays, carrying the true epoch, which closes the stale-reply window of the
   counted epoch; cost one group broadcast per migration, in this mode only.
   Carrying the epoch in MigrateInfo would remove it (later).

Smaller: decompress masks with a 64-bit shift (a dimension may need 31 bits
now); pupLayout writes a marker so a checkpoint without a layout record fails
clearly instead of misreading the readonly messages; the hashed-kind restart
abort distinguishes a changed process count from a changed PE-per-process
layout; the uniqueBits < 16 check moves from initLayout to the first hashed
array's getTranche, so a run with only packed arrays never trips it.

Tests (tests/charm++/objid_insert): -n makes only non-home PEs send the
createhome demand creations, so the creation must come from a message that
reaches the home by id (fails without fix 1 on the packed kind: the elements
are never created, watchdog abort). Phase 4b adds createhere demand creation
with a single sender per element (fails without fix 2 on the packed kind in a
debug build: the null-entry assert). Phase 6 re-creates the elements
destroyed in phase 5 by createhome, from senders holding stale ids and
locations, which is the only path to the home-side index scan of the hashed
kind. Re-creation by createhere is documented as not tested: the creating
PE's insertElement follows its own stale location entry (the isRemote
redirect for bound siblings) and a re-created element restarts at epoch 0,
both of which predate the redesign. The Makefile runs both kinds with and
without -n. lb_test under a -DCMK_GLOBAL_LOCATION_UPDATE=1 debug build with
GreedyLB and RotateLB passes with the fix; it also passed without it on this
laptop (single process, 4 PEs), where the emigrate broadcast lands before
the resume. The window is real on a network, since nothing orders the two;
fix 3 closes it by construction (every PE is updated before the move is
acted on) rather than by a reproduced failure.

Not changed: pendingLocReqs entries can stay set when the home drops a
request for an id it does not know; PR 1b removes requestLocationOnce.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
lvkale added a commit that referenced this pull request Oct 10, 2026
The explicit location requests went away in "a forwarded delivery repairs
the sender's cache"; the helper and its pending set had no callers left.
This closes the last item of Aditya's review of #4017 (an entry in
pendingLocReqs stayed set when the home dropped a request), which #4028
left to this PR.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants