| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | /* | ||
| 2 | * Copyright (c) 2026 Tiger Data, Inc. | ||
| 3 | * Licensed under the PostgreSQL License. See LICENSE for details. | ||
| 4 | * | ||
| 5 | * centroid_build.c - Generic centroid page writer | ||
| 6 | * | ||
| 7 | * Writes centroid entries to linked pages via VsStorage. Format- | ||
| 8 | * agnostic: page format determines metadata and data sizes. | ||
| 9 | */ | ||
| 10 | |||
| 11 | #include <assert.h> | ||
| 12 | #include <math.h> | ||
| 13 | #include <string.h> | ||
| 14 | |||
| 15 | #include "core/memory.h" | ||
| 16 | #include "index/centroid_build.h" | ||
| 17 | #include "quant/fastscan.h" | ||
| 18 | #include "quant/rabitq.h" | ||
| 19 | |||
| 20 | BlockNumber | ||
| 21 | 710 | prism_centroid_write_pages( | |
| 22 | VsStorage *storage, | ||
| 23 | Dimension dim, | ||
| 24 | uint32_t nlist, | ||
| 25 | PrismCentroidFormat fmt, | ||
| 26 | uint8_t level, | ||
| 27 | uint16_t flags, | ||
| 28 | uint16_t child_count, | ||
| 29 | CentroidEncoder *encoder, | ||
| 30 | const BlockNumber *child_blknos, | ||
| 31 | const float *pt_centroids, | ||
| 32 | BlockNumber start_blkno) | ||
| 33 | { | ||
| 34 | 710 | bool reserved = (start_blkno != InvalidBlockNumber); | |
| 35 |
1/2✓ Branch 0 taken 558 times.
✗ Branch 1 not taken.
|
710 | BlockNumber first_blkno = reserved ? start_blkno : InvalidBlockNumber; |
| 36 | 710 | BlockNumber prev_blkno = InvalidBlockNumber; | |
| 37 | 710 | BlockNumber next_blkno = start_blkno; | |
| 38 | 710 | Page cur_page = NULL; | |
| 39 | 710 | BlockNumber cur_blkno = InvalidBlockNumber; | |
| 40 | |||
| 41 | 152 | (void)pt_centroids; /* pt_centroids stored on posting pages, not here */ | |
| 42 | |||
| 43 |
2/2✓ Branch 0 taken 4357 times.
✓ Branch 1 taken 710 times.
|
5067 | for (uint32_t i = 0; i < nlist; i++) |
| 44 | { | ||
| 45 | /* Allocate a new page if needed */ | ||
| 46 |
4/4✓ Branch 0 taken 3647 times.
✓ Branch 1 taken 710 times.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 1067 times.
|
5426 | if (cur_page == NULL || |
| 47 |
2/2✓ Branch 1 taken 24 times.
✓ Branch 2 taken 2554 times.
|
3647 | !prism_centroid_page_has_room(cur_page, dim, false)) |
| 48 | { | ||
| 49 | /* Commit the previous page if any */ | ||
| 50 |
2/2✓ Branch 0 taken 24 times.
✓ Branch 1 taken 558 times.
|
584 | if (cur_page != NULL) |
| 51 | 26 | vs_storage_commit_page(storage, cur_blkno); | |
| 52 | |||
| 53 |
1/2✓ Branch 0 taken 736 times.
✗ Branch 1 not taken.
|
736 | if (reserved) |
| 54 | { | ||
| 55 | 736 | cur_blkno = next_blkno++; | |
| 56 | 736 | cur_page = vs_storage_write_page(storage, cur_blkno); | |
| 57 | } | ||
| 58 | else | ||
| 59 | { | ||
| 60 | ✗ | cur_page = vs_storage_new_page(storage, &cur_blkno); | |
| 61 | } | ||
| 62 | 736 | prism_centroid_page_init_fmt(cur_page, level, fmt); | |
| 63 | |||
| 64 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 736 times.
|
736 | if (first_blkno == InvalidBlockNumber) |
| 65 | ✗ | first_blkno = cur_blkno; | |
| 66 | |||
| 67 | /* Link previous page to this one. The storage layer | ||
| 68 | * holds at most one buffer, so we commit the new page | ||
| 69 | * first, reopen the previous page to set next_blkno, | ||
| 70 | * then reopen the new page. */ | ||
| 71 |
2/2✓ Branch 0 taken 26 times.
✓ Branch 1 taken 710 times.
|
736 | if (prev_blkno != InvalidBlockNumber) |
| 72 | { | ||
| 73 | 26 | vs_storage_commit_page(storage, cur_blkno); | |
| 74 | |||
| 75 | 26 | Page prev_page = vs_storage_write_page(storage, prev_blkno); | |
| 76 | 26 | PRISM_CENTROID_OPAQUE(prev_page)->next_blkno = cur_blkno; | |
| 77 | 26 | vs_storage_commit_page(storage, prev_blkno); | |
| 78 | |||
| 79 | 26 | cur_page = vs_storage_write_page(storage, cur_blkno); | |
| 80 | } | ||
| 81 | |||
| 82 | 736 | prev_blkno = cur_blkno; | |
| 83 | } | ||
| 84 | |||
| 85 | 9935 | BlockNumber entry_child = child_blknos != NULL ? child_blknos[i] | |
| 86 |
1/2✓ Branch 0 taken 4357 times.
✗ Branch 1 not taken.
|
4357 | : InvalidBlockNumber; |
| 87 | |||
| 88 | 4357 | void *dest = prism_centroid_page_add_entry_begin( | |
| 89 | cur_page, dim, entry_child, child_count, flags); | ||
| 90 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 4357 times.
|
4357 | assert(dest != NULL); |
| 91 | 4357 | encoder->ops->encode_into(encoder, i, dest); | |
| 92 | } | ||
| 93 | |||
| 94 | /* Commit last page */ | ||
| 95 |
1/2✓ Branch 0 taken 710 times.
✗ Branch 1 not taken.
|
710 | if (cur_page != NULL) |
| 96 | 710 | vs_storage_commit_page(storage, cur_blkno); | |
| 97 | |||
| 98 | 710 | return first_blkno; | |
| 99 | } | ||
| 100 | |||
| 101 | /* ---------------------------------------------------------------- | ||
| 102 | * FASTSCAN centroid writer | ||
| 103 | * | ||
| 104 | * Walks the input centroid list in 32-vector groups. For each group: | ||
| 105 | * 1. Encode each centroid with vs_rabitq_encode_into to get | ||
| 106 | * f_add, f_rescale, and 1-bit packed code bytes. | ||
| 107 | * 2. Derive f_error from (f_add, f_rescale). | ||
| 108 | * 3. Repack the bits into the kPerm0 layout that | ||
| 109 | * vs_fastscan_accumulate_hacc expects, and place the per- | ||
| 110 | * group f_add / f_rescale / f_error / child_blkno arrays in | ||
| 111 | * the group section. | ||
| 112 | * | ||
| 113 | * One page holds `groups_per_page` groups; once a page is full we | ||
| 114 | * commit it and chain via next_blkno, the same way the RABITQ | ||
| 115 | * writer above does. Partial trailing group is padded with | ||
| 116 | * InvalidBlockNumber and zero codes. | ||
| 117 | * ---------------------------------------------------------------- */ | ||
| 118 | |||
| 119 | BlockNumber | ||
| 120 | 1402 | prism_centroid_write_fastscan_pages( | |
| 121 | VsStorage *storage, | ||
| 122 | Dimension dim, | ||
| 123 | uint32_t nlist, | ||
| 124 | uint8_t level, | ||
| 125 | uint16_t flags, | ||
| 126 | const RaBitQParams *params, | ||
| 127 | const float *vectors, | ||
| 128 | const float *global_mean, | ||
| 129 | const BlockNumber *child_blknos, | ||
| 130 | BlockNumber start_blkno) | ||
| 131 | { | ||
| 132 | 1294 | (void)flags; /* per-entry flags not stored in FASTSCAN format */ | |
| 133 | |||
| 134 | 1402 | uint32_t packed_bytes = (dim + 7) / 8; | |
| 135 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1294 times.
|
1402 | uint32_t groups_per_page = prism_centroid_fastscan_max_groups(dim); |
| 136 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1402 times.
|
1402 | if (groups_per_page == 0) |
| 137 | ✗ | groups_per_page = 1; /* defensive — bigger dims may need split */ | |
| 138 | |||
| 139 | 1402 | uint32_t ngroups = (nlist + VS_FASTSCAN_GROUP - 1) / VS_FASTSCAN_GROUP; | |
| 140 | |||
| 141 | /* Scratch for one group's RaBitQ data (32 entries). */ | ||
| 142 | 1402 | size_t rdata_size = VS_RABITQ_DATA_SIZE(dim); | |
| 143 | 1402 | uint8_t *rdata_buf = vs_alloc((size_t)VS_FASTSCAN_GROUP * rdata_size); | |
| 144 | 1402 | uint8_t *bits_buf = vs_alloc((size_t)VS_FASTSCAN_GROUP * packed_bytes); | |
| 145 | |||
| 146 | 1402 | Vec32Ref mref = {.data = global_mean, .dim = dim}; | |
| 147 | |||
| 148 | 1402 | bool reserved = (start_blkno != InvalidBlockNumber); | |
| 149 |
1/2✓ Branch 0 taken 108 times.
✗ Branch 1 not taken.
|
1402 | BlockNumber first_blkno = reserved ? start_blkno : InvalidBlockNumber; |
| 150 | 1402 | BlockNumber prev_blkno = InvalidBlockNumber; | |
| 151 | 1402 | BlockNumber next_blkno = start_blkno; | |
| 152 | 1402 | Page cur_page = NULL; | |
| 153 | 1402 | BlockNumber cur_blkno = InvalidBlockNumber; | |
| 154 | 1402 | uint32_t cur_ngroups = 0; | |
| 155 | |||
| 156 |
2/2✓ Branch 0 taken 1426 times.
✓ Branch 1 taken 1402 times.
|
2828 | for (uint32_t g = 0; g < ngroups; g++) |
| 157 | { | ||
| 158 | /* Open a fresh page when needed */ | ||
| 159 |
3/4✓ Branch 0 taken 1318 times.
✓ Branch 1 taken 108 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 24 times.
|
1426 | if (cur_page == NULL || cur_ngroups == groups_per_page) |
| 160 | { | ||
| 161 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1402 times.
|
1402 | if (cur_page != NULL) |
| 162 | ✗ | vs_storage_commit_page(storage, cur_blkno); | |
| 163 | |||
| 164 |
1/2✓ Branch 0 taken 1402 times.
✗ Branch 1 not taken.
|
1402 | if (reserved) |
| 165 | { | ||
| 166 | 1402 | cur_blkno = next_blkno++; | |
| 167 | 1402 | cur_page = vs_storage_write_page(storage, cur_blkno); | |
| 168 | } | ||
| 169 | else | ||
| 170 | ✗ | cur_page = vs_storage_new_page(storage, &cur_blkno); | |
| 171 | |||
| 172 | 1402 | prism_centroid_page_init_fmt( | |
| 173 | cur_page, level, PRISM_CENTROID_FMT_FASTSCAN); | ||
| 174 | |||
| 175 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1402 times.
|
1402 | if (first_blkno == InvalidBlockNumber) |
| 176 | ✗ | first_blkno = cur_blkno; | |
| 177 | |||
| 178 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1402 times.
|
1402 | if (prev_blkno != InvalidBlockNumber) |
| 179 | { | ||
| 180 | ✗ | vs_storage_commit_page(storage, cur_blkno); | |
| 181 | ✗ | Page prev = vs_storage_write_page(storage, prev_blkno); | |
| 182 | ✗ | PRISM_CENTROID_OPAQUE(prev)->next_blkno = cur_blkno; | |
| 183 | ✗ | vs_storage_commit_page(storage, prev_blkno); | |
| 184 | ✗ | cur_page = vs_storage_write_page(storage, cur_blkno); | |
| 185 | } | ||
| 186 | |||
| 187 | 1402 | prev_blkno = cur_blkno; | |
| 188 | 1402 | cur_ngroups = 0; | |
| 189 | } | ||
| 190 | |||
| 191 | /* Encode this group's 32 entries (or fewer for the last). */ | ||
| 192 | 1426 | uint32_t g_start = g * VS_FASTSCAN_GROUP; | |
| 193 | 1426 | uint32_t g_count = nlist - g_start; | |
| 194 |
2/2✓ Branch 0 taken 24 times.
✓ Branch 1 taken 108 times.
|
1426 | if (g_count > VS_FASTSCAN_GROUP) |
| 195 | 24 | g_count = VS_FASTSCAN_GROUP; | |
| 196 | |||
| 197 | /* Get per-entry RaBitQ encoding into rdata_buf, copy bits | ||
| 198 | * out into a flat packed_bytes-stride array for the packer. */ | ||
| 199 |
2/2✓ Branch 0 taken 10916 times.
✓ Branch 1 taken 1426 times.
|
12342 | for (uint32_t v = 0; v < g_count; v++) |
| 200 | { | ||
| 201 | 10916 | Vec32Ref vref = { | |
| 202 | 10916 | .data = vectors + (size_t)(g_start + v) * dim, | |
| 203 | .dim = dim, | ||
| 204 | }; | ||
| 205 | 10916 | RaBitQData *d = (RaBitQData *)(rdata_buf + (size_t)v * rdata_size); | |
| 206 | 10916 | vs_rabitq_encode_into(params, vref, mref, d); | |
| 207 | 10916 | memcpy(bits_buf + (size_t)v * packed_bytes, d->bits, packed_bytes); | |
| 208 | } | ||
| 209 | |||
| 210 | /* Write per-entry scalars + child_blkno arrays into the | ||
| 211 | * group section, then pack codes. */ | ||
| 212 | 1426 | uint32_t cur_group_idx = cur_ngroups; | |
| 213 | 1426 | char *content = (char *)PageGetContents(cur_page); | |
| 214 | |||
| 215 | 1426 | BlockNumber *child = prism_centroid_fastscan_group_child( | |
| 216 | content, cur_group_idx, dim); | ||
| 217 | 1426 | float *f_add_arr = prism_centroid_fastscan_group_f_add( | |
| 218 | content, cur_group_idx, dim); | ||
| 219 | 1426 | float *f_rescale_arr = prism_centroid_fastscan_group_f_rescale( | |
| 220 | content, cur_group_idx, dim); | ||
| 221 | 1426 | float *f_error_arr = prism_centroid_fastscan_group_f_error( | |
| 222 | content, cur_group_idx, dim); | ||
| 223 | 1426 | uint8_t *codes = prism_centroid_fastscan_group_codes( | |
| 224 | content, cur_group_idx, dim); | ||
| 225 | |||
| 226 |
2/2✓ Branch 0 taken 45632 times.
✓ Branch 1 taken 1426 times.
|
47058 | for (uint32_t v = 0; v < VS_FASTSCAN_GROUP; v++) |
| 227 | { | ||
| 228 |
2/2✓ Branch 0 taken 10916 times.
✓ Branch 1 taken 34716 times.
|
45632 | if (v < g_count) |
| 229 | { | ||
| 230 | 10916 | const RaBitQData *d = (const RaBitQData *)(rdata_buf + | |
| 231 | 10916 | (size_t)v * | |
| 232 | rdata_size); | ||
| 233 | 31414 | child[v] = child_blknos != NULL ? child_blknos[g_start + v] | |
| 234 |
1/2✓ Branch 0 taken 10916 times.
✗ Branch 1 not taken.
|
10916 | : InvalidBlockNumber; |
| 235 | 10916 | f_add_arr[v] = d->f_add; | |
| 236 | 10916 | f_rescale_arr[v] = d->f_rescale; | |
| 237 | 10916 | f_error_arr[v] = | |
| 238 | 10916 | vs_rabitq_derive_f_error(d->f_add, d->f_rescale, dim); | |
| 239 | } | ||
| 240 | else | ||
| 241 | { | ||
| 242 | 34716 | child[v] = InvalidBlockNumber; | |
| 243 | 34716 | f_add_arr[v] = 0.0f; | |
| 244 | 34716 | f_rescale_arr[v] = 0.0f; | |
| 245 | 34716 | f_error_arr[v] = 0.0f; | |
| 246 | } | ||
| 247 | } | ||
| 248 | |||
| 249 | 1426 | vs_fastscan_pack_codes(bits_buf, g_count, dim, codes); | |
| 250 | |||
| 251 | 1426 | PrismCentroidPageOpaque *op = PRISM_CENTROID_OPAQUE(cur_page); | |
| 252 | 1426 | op->entry_count += (uint16_t)g_count; | |
| 253 | 1426 | cur_ngroups++; | |
| 254 | |||
| 255 | /* Bump pd_lower so PostgreSQL's hole-compression preserves | ||
| 256 | * our writes. FASTSCAN doesn't use the backward data region | ||
| 257 | * — everything lives in the forward content area. */ | ||
| 258 | 1426 | PageHeader header = (PageHeader)cur_page; | |
| 259 | 1426 | header->pd_lower += prism_centroid_fastscan_group_bytes(dim); | |
| 260 | } | ||
| 261 | |||
| 262 |
1/2✓ Branch 0 taken 1402 times.
✗ Branch 1 not taken.
|
1402 | if (cur_page != NULL) |
| 263 | 1402 | vs_storage_commit_page(storage, cur_blkno); | |
| 264 | |||
| 265 | 1402 | vs_free(rdata_buf); | |
| 266 | 1402 | vs_free(bits_buf); | |
| 267 | |||
| 268 | 1402 | return first_blkno; | |
| 269 | } | ||
| 270 |