feat: GPU HNSW search with parallel beam-search kernel — native FP32/FP16/BF16/INT8#1
feat: GPU HNSW search with parallel beam-search kernel — native FP32/FP16/BF16/INT8#1devin-ai-integration[bot] wants to merge 22 commits into
Conversation
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
GPU HNSW index for faiss (GpuIndexHNSW): build + OCQ beam-search kernel, int8/fp16/fp32 support. Clean feature-only commit on current main; dead cooperative-distance code (select_threads_per_dist/coop_l2_distance/ coop_ip_distance, zero callers) excluded. Signed-off-by: Devin AI <devin-ai-integration[bot]@users.noreply.github.com>
2708bc1 to
b8de409
Compare
- default overflow_factor to 2 in internal GpuHnswSearchParams so the default search path keeps the overflow candidate queue enabled - propagate config_.device into the scratch pool instead of hardcoding device 0 (fixes cross-device streams on non-default devices) - make setSearchParams sticky (no consume-once race under concurrency) - align copyFrom header signature with the knowhere::IndexHNSW impl and declare copyFromWithMetric - query the device shared-memory opt-in limit and opt the kernel into >48KiB dynamic shared memory instead of assuming a 48KiB cap - synchronize before the stack-local entry-point buffer is destroyed Signed-off-by: premal <premal@6sense.com> Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The overflow candidate queue was never wired in: overflow_insert() had no call site and d_overflow_count was only ever zeroed and read, so the num_parents==0 fallback always no-oped. It only allocated nq*overflow_factor*ef scratch VRAM + a per-search memset for a queue that was never populated. Remove overflow_factor from the public/internal search params, the d_overflow_* scratch buffers, overflow_insert(), and the dead fallback. The kernel is a plain parallel beam search; raise ef for more recall. Signed-off-by: premal <premal@6sense.com> Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…n record The overflow candidate queue was never functional (overflow_insert had no call site; d_overflow_count only zeroed+read). Removed from the kernel in 6si/faiss#1 and 6si/knowhere#2. Update the design doc to describe a plain parallel beam search and add a decision record explaining the removal. Signed-off-by: premal <premal@6sense.com> Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Mirror of 6si/faiss#1: the overflow candidate queue was never wired in (overflow_insert had no call site; d_overflow_count only zeroed+read), so it only wasted scratch VRAM + a memset per search. Remove the overflow machinery from the vendored faiss GPU kernel and drop the explicit gsp.overflow_factor=2 in faiss_hnsw.cc. Recall is tuned via ef. Signed-off-by: premal <premal@6sense.com> Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…em clamp) - searchImpl_: make the scratch-pool stream wait on the resources' default stream (event) before reading queries copied there by GpuIndex::search, fixing a host-query race (finding facebookresearch#15). - GpuHnswSearchScratch/GpuHnswDeviceIndex: record the owning device and cudaSetDevice() before cudaFree in their destructors (multi-GPU, facebookresearch#19/facebookresearch#20). - GpuHnswSearch: throw a clear error when search_width is too large to fit even ef=1 in shared memory instead of clamping ef negative (facebookresearch#18). Findings facebookresearch#17 (level off-by-one) and facebookresearch#21/facebookresearch#22 (IP/cosine sign) are false positives: this fork uses the standard FAISS levels[] convention and Knowhere un-negates IP/COSINE and normalizes cosine queries around searchHost. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: premal <premal@6sense.com> Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Add native fp16/bf16 support to GpuIndexHNSW, mirroring the existing int8 native path. fp16/bf16 HNSW_SQ codes (QT_fp16/QT_bf16) are stored raw as IEEE half/bfloat16 (2 bytes/element), bit-compatible with CUDA half / __nv_bfloat16, and uploaded verbatim to the device (no up-conversion to fp32 at upload, preserving the memory win). The search kernel dispatches on a new GpuHnswDatasetType enum (FP32/INT8/FP16/BF16) and the templated distance helpers select the matching load_elem overload, up-converting each element to fp32 for accumulation. Cosine inverse-L2-norms are computed from the fp32-decoded values (mirroring int8). Graph-walk algorithm unchanged. Compile-verified only; GPU-runtime validation pending capacity. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: premal <premal@6sense.com> Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…strings Address external review findings on the GPU HNSW code: - H1: add cudaStreamSynchronize safety net in GpuHnswScratchPool::release so a slot's stream is drained before the slot is reused; guards against a caller that skips its own sync (latent use-after-free via ensure() free+realloc). - L2: GpuIndexHNSW docstring updated to list fp16/bf16 (native 2-byte device storage) alongside float32/int8, and copyFrom SQ type list. - C1: document that searchHost is the preferred entry point and searchImpl_ round-trips labels D2H->H2D; a GPU-side label-conversion kernel is a follow-up. Knowhere uses searchHost. - H5: document the HnswT levels[] 1-based-count convention required by extract_hnsw_layers. - L1: document num_layers as informational (search uses num_upper_layers_built). Signed-off-by: premal <premal@6sense.com> Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
Pushed
Other review items were verified as already-correct or intentional (const_cast, cudaSetDevice-in-dtor, ef-not-validated matching the CPU checker). The C1 GPU-kernel label-conversion and the cosine-recall investigation (H4) are tracked as follow-ups; GPU unit tests are running now on an L40S. |
The CPU cosine index records inverse L2 norms from the ORIGINAL input vectors (HasInverseL2Norms::get_inverse_l2_norms()). Recomputing norms from decoded/ quantized codes (or normalizing decoded fp32 vectors in place) diverges from the CPU index's scores and HNSW traversal for lossy SQ8/int8/fp16/bf16 cosine. Thread the stored inverse norms from from_faiss_hnsw_sq into the int8/halfwidth/fp32 upload paths and apply them in the kernel; only flat-fp32 cosine (decoded == original) keeps normalizing in place. Byte-identical with the knowhere vendored copy. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: premal <premal@6sense.com> Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Codex P1 #1 — upload CPU inverse L2 norms for quantized cosine (
|
Add GpuHnswUploadFaultInjection (host-safe, in GpuHnswTypes.h): a countdown that makes the Nth GPU_HNSW_BUILD_CUDA_CHECK in the device-upload path report a simulated failure. Never armed in production (countdown 0 -> one relaxed atomic load per wrapped call, no effect); lets Knowhere unit tests exercise the upload error/cleanup/retry path without a real OOM. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: premal <premal@6sense.com> Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
node_ids is read-only after construction, so copy directly from it in the cudaMemcpy instead of duplicating into h_node_ids. Behavior-neutral. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: premal <premal@6sense.com> Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Phase 1 optimization: all 128 threads now participate in both the staging sort and result merge instead of thread 0 doing all the work while the other 127 idle. Changes: - GpuHnswSearchKernel.cuh: add bitonic_sort_staging (128-element, 128-thread, in-place) and parallel_merge_into_result (binary-search per output slot, O(log N) divergence) as replacements for the serial sorted-insert loop; add merged_ids/merged_dists/merged_expanded scratch arrays to shared memory; update calc_layer0_smem_size - GpuHnswSearch.cuh: update smem_overhead formula (24 bytes/ef slot instead of 12, adds 3 merge scratch arrays); add power-of-2 check on search_width × max_degree0 required by bitonic sort Three correctness bugs fixed during development: 1. Bitonic sort race: each thread now writes only to slot i (not partner), eliminating inter-warp write-write races (778K instances detected by compute-sanitizer --racecheck) 2. Negative ri index: binary search exit can yield si = min(pos,sc)+1 giving ri = pos-si = -1; added ri_valid guard and ri_safe = max(0,ri) clamp on all array reads 3. Speculative -O2 loads: nvcc speculatively evaluates array loads even when the branch predicate is false; fixed by clamping ri_mid = max(0, pos-mid) in the binary search body and adding an explicit sentinel override when pos < mid Measured results (milvus-gpu-hnsw cluster, mpd_v2 570M INT8, 8 querynodes, v76 serial baseline vs v78 this commit): Single client: batch=256: 4,638 vec/s vs 4,022 (+15%), p50 52ms vs 63ms (-17%) batch=128: 3,783 vec/s vs 3,685 (+3%), p50 33ms vs 32ms (flat) 8 parallel clients (production-representative): batch=64: 11,181 vs 9,919 vec/s (+13%) batch=128: 14,720 vs 9,986 vec/s (+47%) batch=256: 17,432 vs 7,355 vec/s (+137%) ncu barrier stall: 69.89% → 39.67% (-30pp) ncu long-scoreboard stall: 4.77% → 21.01% (+16pp, now new bottleneck) Remaining stall is global memory latency from distance compute (__ldg on INT8 dataset), not the merge — next optimization target. Recall unchanged: R@1=0.899, R@10=0.937 across all configurations. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… merge Forward-port Knowhere's native int8 DP4A search into faiss's parallel bitonic-merge kernel so the two GPU HNSW trees stop diverging. The two layer-0 kernels (serial int8 + parallel fp32) collapse into one kernel templated on <DataT, QueryT, USE_DP4A>: - generic path: QueryT=float, USE_DP4A=false (fp32/fp16/bf16/int8 via load_elem, L2 or negated-IP); - native int8 path: DataT=QueryT=int8_t, USE_DP4A=true, thread_ip_distance_dp4a. Both share the same shared-memory layout and parallel_merge_into_result. Adds searchHostInt8() (uploads signed int8 queries verbatim, no shift; fp32 fallback when dim%4!=0) plus the d_queries_i8 scratch field/ensure() arg, and INT8 DP4A dispatch (int8 + inner-product/cosine + dim%4==0). Review fixes folded in: - pad layer-0 staging to next_pow2(search_width*max_degree0) and grow the block to match, instead of hard-failing when 2*M is not a power of two; throw a clear error only if the padded capacity exceeds the 1024 block limit; - warn (stderr) when ef is clamped to fit shared memory instead of silently truncating; - document the negated IP/cosine distance convention and the (corrected, no +128 shift) int8 query encoding in GpuIndexHNSW.h. Signed-off-by: premal <premal@6sense.com> Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Code Review — Delta (DP4A + Padded Staging)
|
…esult The diagonal binary search compared staging[mid] against result[pos-mid] instead of result[pos-mid-1], mis-partitioning every merge (~4% recall on mpd_v2) and, with padded staging, promoting a UINT32_MAX sentinel into the result list that was then used as a graph/dataset index -> CUDA illegal memory access (the v81 crash at GpuIndexHNSW.cu:332). Fix uses ri_mid = pos-mid-1 with a -inf boundary when ri_mid<0. Validated on CPU against std::merge over 200k random (rc,sc,ef) cases: 0 failures (was 187330/200000). Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…raph indexing Bounds-check every node id before it indexes d_layer0_graph or d_dataset in layer0_beam_search_kernel: - entry point (ep): skip seeding + neighbor scan if ep >= N (ep_valid gate) - parent id: skip expansion if parent >= N (mirrors the existing nbr guard) - top-k output: emit UINT64_MAX sentinel if result_ids[i] >= N Prevents the v82 fp16 P1 OOB (illegal global read in layer0_beam_search_kernel<__half,float,false>, addr ~= bad_id*max_degree0*4) from a stray/garbage node id reaching graph/dataset indexing. On the healthy path (valid ep, correct merge) none of these fire, so recall is unchanged. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: premal <premal@6sense.com> Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…pare-exchange) Split the bitonic compare-exchange into two barrier-separated phases. Each thread owns writes to slot i but READS slot partner=i^j; for partners in a different warp (j >= warpSize) that read raced with partner's write to its own slot in the same phase (racecheck: ~1.6M hazards in <__half,float,false>, ~664K in <float,float,false>). Now: read pair + decide swap -> __syncthreads -> conditional write of own slot -> __syncthreads. This is the actual root cause of the v82 fp16 P1 OOB (the stale read produced a garbage node id); fp32 hit the same race but only OOB'd at large N. Sort result is unchanged (canonical two-phase bitonic); loop bounds stay uniform so every thread reaches every barrier. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: premal <premal@6sense.com> Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Code Review — Delta 2 (Bitonic Sort Race Fix + Defensive Guards)Both bug fixes verified correct via brute-force simulation1. Bitonic sort race fix — ✅ Correct
2. Merge-path
This was a real correctness bug, not just a defensive guard. 3. Defensive guards — ✅ Correct, don't mask live bugs Still Open (carried forward)
Verified via Python simulation (no CUDA/GPU available for compile/test). |
…amp ef>=k - Negate IP/COSINE score on kernel copy-out so the public API returns true similarity (larger == closer), matching faiss METRIC_INNER_PRODUCT; make the padding sentinels metric-aware (-FLT_MAX for IP/COSINE, FLT_MAX for L2). - Validate ef>0 and search_width>0 before computing the default iteration count (prevents divide-by-zero). - Auto-clamp ef=max(ef,k) so k results are always tracked; retain the shared-memory-fit guard that throws when k itself cannot fit. - Cache cudaFuncSetAttribute per high-water dynamic shared-memory size. - Set the owning device before destroying a scratch stream (multi-GPU). - Remove unused <atomic> from GpuIndexHNSW.h; correct the public distance documentation; wrap the long merge-path line. Signed-off-by: premal <premal@6sense.com> Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Code Review — Delta 3 (k<=ef clamp, smem caching, distance convention)Previously-Flagged Items — All Addressed
New Issue — Important (should-fix)cudaFuncSetAttribute caching TOCTOU race: The Verified via Python simulation. Fix: CAS loop or mutex to ensure monotonic increase — only set the attribute if the new smem_size is strictly greater than the current Distance Convention Change — CorrectThe copy-out now negates IP/cosine back to true similarity ( Degenerate Param Validation — Correct
No new correctness bugs. Static analysis only (no CUDA/GPU for compile/test). |
…pose metric - Replace the racy std::atomic load-check-store around cudaFuncSetAttribute with a mutex-guarded, monotonic high-water update. Two concurrent searches with different smem sizes could interleave so a smaller-ef call ran after a larger one and downgraded the kernel's global max-dynamic-smem attribute while a recorded high-water mark said otherwise, making a later intermediate launch fail. Under the lock the attribute only ever grows and is always >= the current launch's requirement. - Validate hnsw.entry_point in [0, n_rows) in extract_hnsw_layers before it is used to index d_dataset on device, turning a malformed/sentinel entry_point into a host-side throw instead of an OOB device read. - Add GpuIndexHNSW::isCosine()/useInnerProduct() accessors, set at copyFrom()/copyFromWithMetric() time, so callers can post-process results from the index-side metric instead of a per-search config that may omit metric_type (and default to L2). Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Signed-off-by: premal <premal@6sense.com> Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Code Review — Delta 4 (TOCTOU mutex fix + entry_point validation + isCosine API)All Previously-Flagged Should-Fix Items Now Resolved
No New IssuesNo critical or important issues. Minor: All previously-flagged should-fix items from our review series are now resolved. No blockers for merge. |
reset() left is_cosine_/use_ip_ at their prior copyFrom() values. Safe today (searches guard on is_trained), but a subsequent copyFrom() of an L2 index would inherit stale cosine/IP interpretation. Clear them so reset() fully returns the index to an untrained, metric-neutral state. Signed-off-by: premal <premal@6sense.com> Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Code Review — Delta 5 (reset() clears metric fields)✅ M1 from Delta 4 — FIXED
No new issues. All previously-flagged items remain resolved. |
…set) Add GPU-side bitset filtering to layer-0 HNSW search so GPU_HNSW serves segments with deletes/TTL/partition visibility, matching CPU HNSW rather than rejecting a non-empty bitset. Kernel: is_bitset_filtered() device test (byte id/8, bit id%8, LSB-first, matches Knowhere BitsetView); filtered rows stay graph waypoints but are never emitted. Two-tier beam (valid ef + invalid frontier ef_inv) with serialized post-sort alpha gate (accumulated_alpha per query in meta[4], init 1.0f; kAlpha=filter_ratio*0.7) and cross-beam parent selection (globally-closest unexpanded across both beams). HAS_FILTER template param; unfiltered path codegen-identical. Brute force (GpuHnswBruteForce.cuh): GPU-resident up-front BF (>=0.93 filtered or k>=0.5*live) and per-query fallback via device needs_bf[] worklist (no host round trip). Host: per-slot d_bitset/d_needs_bf scratch (grow-only ensure_filter), bitset upload before launch, needs_bf_count zeroed via cudaMemsetAsync, smem re-budget (36 B/ef filtered), disable_fallback_brute_force threading. Threaded through all 5 dtypes. Signed-off-by: premal <premal@6sense.com> Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…GPU) Signed-off-by: premal <premal@6sense.com> Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
feat(gpu_hnsw): CPU-parity filtered search (deletes/TTL/partition bitset)
Delta 6 Review — faiss PR #1 (filtered search merged in)The filtered search implementation from PR #7 has been folded into PR #1. Reviewed the delta against our previous PR #7 review. Previously-Flagged Items
New Issues (both minor)
VerdictNo blockers. Ready for compilation and the full recall/sanitizer gate. The I1 fix is the key item and it's resolved. The integration is clean — Full review at $M/reports/review-faiss-pr1-delta6.md |
The layer-0 expansion computed each candidate's full-dim distance on a single thread, so a warp's 32 lanes read 32 different vectors (rows dim apart) -> ncu measured 12.1 sectors/request (~8% coalescing), 49% of warp stalls on global load latency, and DRAM at only 37.6% of peak. Assign one warp per candidate edge: lane 0 resolves the neighbor id and claims the visited-bitmap first-visit, broadcasts go/no-go + id, then all 32 lanes stride the vector and warp-reduce the partial sums (DP4A int8, generic IP, and L2). Loads coalesce toward 1 sector/request on the dominant traffic source. The predicate is warp-uniform so the warp never diverges across the __shfl reductions. Host rounds block_size up to a whole number of warps. Distance values, staging contents, and result ordering are unchanged (only the parallelization of the identical computation changes); correctness/recall to be confirmed by the existing recall + sanitizer suite. Signed-off-by: premal <premal@6sense.com> Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
perf(gpu_hnsw): warp-cooperative layer-0 distance (coalesced dataset loads)
|
Superseded: we re-cut clean gpu-hnsw branches from v2's base. Current PR is #11. Closing; branch retained. |
Summary
Adds
GpuIndexHNSW— a GPU implementation of HNSW search using a custom parallel beam-search kernel. Accepts a CPU-builtIndexHNSW/IndexHNSWSQviacopyFrom()/copyFromWithMetric(), uploads vectors + graph to VRAM, frees the CPU copy, and runs search entirely on GPU. Recall is tuned viaef.The device dataset is stored in four native precisions, each kept in its on-disk layout (no up-conversion at upload) and up-converted to fp32 per element inside the kernel for distance accumulation:
load_elemfloatQT_fp16__half2floatQT_bf16__bfloat162floatQT_8bit_direct_signedstatic_cast<float>Dispatch is driven by an explicit enum instead of a single bool flag:
FP16/BF16 mirror the pre-existing INT8 native path:
from_faiss_hnsw_sq()copies the raw 2-byte SQ codes verbatim viaupload_halfwidth_dataset()(bit-compatible with CUDAhalf/__nv_bfloat16), and for COSINE computesd_inv_normsfrom the fp32-decoded values (stored codes are not normalized). The templated distance helpers (thread_l2_distance/thread_ip_distance) were alreadyDataT-generic; only the newload_elemoverloads and the dispatch were added. The graph-walk algorithm is unchanged.Key design decisions:
Parallel beam-search kernel (
GpuHnswSearchKernel.cuh): Layer-0 search uses a sorted result buffer (efentries in smem) + a staging buffer (sw × max_degree0) merged by thread 0 via insertion sort. Stagnation is detected vianum_parents == 0(early exit). An earlier "OCQ" overflow queue was removed as dead code — see the decision recorddocs/design-docs/design_docs/20260711-gpu-hnsw-ocq-removal.mdin 6si/milvus#6. Use a largereffor higher recall.1-thread-per-distance: Each of 128 threads computes one full distance independently — 128 concurrent distances vs 32 with 4-thread cooperative; the concurrency outweighs coalescing at these vector sizes.
Native low-precision storage: FP16/BF16 (2 bytes) and INT8 (1 byte) stay in their native layout on device — 2×/4× less VRAM than fp32 (critical at tens of millions of rows/node) — with per-element up-conversion to fp32 in the kernel.
Per-segment streams + scratch pool: each
GpuHnswDeviceIndexowns acudaStreamNonBlocking; searches acquire aGpuHnswSearchScratchslot (+ stream) fromGpuHnswScratchPoolfor concurrent execution across Milvus segments.Host API:
Verification: compile-verified via the GPU build; GPU-runtime recall/latency/VRAM validated on-cluster — see Performance below and the unit/recall/sanitizer gates (fp32/fp16/bf16/int8 × L2/IP/COSINE).
Performance (CPU-HNSW vs GPU)
Benchmarked on the same dataset and harness (collection
mpd_v2, 569.9M rows, INT8 COSINE;run_eval_jobs.py,--sample-size 2048 --continuous-iterations 30). Best cell = batch 512, 8 parallel × 4 workers (32 concurrency):Throughput figures are from the
gpu-hnsw-v89image (this beam-search kernel; fp32/int8 COSINE). v89 is 10–24% faster than v83 at all matched batch-512 cells (4×1 +11%, 4×4 +11%, 8×1 +24%, 8×4 +14%), so the filtered-search path does not regress unfiltered throughput. Native fp16/bf16 throughput is a follow-up; correctness across fp32/fp16/bf16/int8 × L2/IP/COSINE (and filtered deletes/TTL/partition at CPU parity) is covered by the unit/recall/sanitizer gates and the sealed-segment cluster e2e.Related PRs: 6si/knowhere#2 (Knowhere wrapper + registrations), 6si/milvus#6 (Milvus integration).
Link to Devin session: https://6sense.devinenterprise.com/sessions/d39aba56b8a3467cbbf231ab631a06ed
Link to Devin session: https://6sense.devinenterprise.com/sessions/55dab0bd33c346df99b5818b30059342
Requested by: @premal