Skip to content

fix: ensure single shared SyncPrefixHashTable instance across Store a…#2096

Merged
varungup90 merged 2 commits intovllm-project:mainfrom
penfree:bugfix/syncindexer
Apr 11, 2026
Merged

fix: ensure single shared SyncPrefixHashTable instance across Store a…#2096
varungup90 merged 2 commits intovllm-project:mainfrom
penfree:bugfix/syncindexer

Conversation

@penfree
Copy link
Copy Markdown
Contributor

@penfree penfree commented Apr 11, 2026

Fix: Ensure Single Shared SyncPrefixHashTable Instance Across Components

Problem

Multiple components created separate instances of SyncPrefixHashTable, leading to inconsistent prefix cache state:

  • Store (pkg/cache/cache_init.go:566) created its own instance
  • kvSyncPrefixCacheRouter (pkg/plugins/gateway/algorithms/prefix_cache.go:303) created another instance

Impact:

  • ❌ Prefix cache state divergence between gateway and storage layers
  • ❌ Cache misses due to different indexer instances
  • ❌ Inconsistent KV sync behavior

Example Scenario

Request Flow:
  1. kvSyncRouter adds prefix to its SyncPrefixHashTable (instance A)
  2. Store's KV event handler updates its SyncPrefixHashTable (instance B)
  3. Next request queries instance A, but prefix data is in instance B
  4. Result: Cache miss ❌

Root Cause

The SyncPrefixHashTable was designed to be a shared, centralized index. However, the code directly called NewSyncPrefixHashTable() in multiple locations without ensuring a single shared instance.

Solution

Implement singleton pattern to guarantee all components reference the same instance:

1. Add Singleton Function

File: pkg/utils/syncprefixcacheindexer/sync_hash.go

var (
    sharedSyncOnce     sync.Once
    sharedSyncInstance *SyncPrefixHashTable
)

// GetSharedSyncPrefixHashTable returns the shared singleton instance
func GetSharedSyncPrefixHashTable() *SyncPrefixHashTable {
    sharedSyncOnce.Do(func() {
        sharedSyncInstance = NewSyncPrefixHashTable()
    })
    return sharedSyncInstance
}

Features:

  • Thread-safe initialization using sync.Once
  • Lazy initialization on first access
  • Follows the same pattern as GetSharedPrefixHashTable

2. Update All Call Sites

# pkg/cache/cache_init.go
- s.syncPrefixIndexer = syncindexer.NewSyncPrefixHashTable()
+ s.syncPrefixIndexer = syncindexer.GetSharedSyncPrefixHashTable()

# pkg/plugins/gateway/algorithms/prefix_cache.go
  kvSyncRouter := &kvSyncPrefixCacheRouter{
-     syncIndexer:    syncindexer.NewSyncPrefixHashTable(),
+     syncIndexer:    syncindexer.GetSharedSyncPrefixHashTable(),
  }

Testing

New Test Suite

Added pkg/utils/syncprefixcacheindexer/singleton_test.go with tests for:

  1. Single Instance Verification - Multiple calls return the same instance
  2. Concurrent Access Safety - 100 goroutines verify thread-safe initialization
  3. Functionality Persistence - Data persists across different references

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request transitions the SyncPrefixHashTable to a singleton pattern to ensure consistent indexing across different components such as the Store and the gateway router. While the implementation correctly uses sync.Once for thread-safe initialization, the transition introduces a significant lifecycle risk: calling Close() on the shared instance (for example, during Store cleanup) will permanently stop the background eviction worker for all components without a way to restart it. Additionally, the new unit tests should be updated to close the existing instance before resetting the singleton state to prevent leaking background goroutines.

…nd kvSyncRouter

Problem:
Previously, Store and kvSyncPrefixCacheRouter created separate instances
of SyncPrefixHashTable, leading to inconsistent prefix cache state and
potential cache misses.

Solution:
Implement singleton pattern for SyncPrefixHashTable:
- Add GetSharedSyncPrefixHashTable() function
- Use sync.Once to ensure only one instance is created
- Update Store (cache_init.go) to use shared instance
- Update kvSyncRouter (prefix_cache.go) to use shared instance

Lifecycle Management:
- Store.cleanupKVEventSync() no longer closes the shared singleton
- This prevents breaking other components (e.g., gateway router) that
  may still be using the shared instance
- The singleton relies on process exit for cleanup (consistent with
  current behavior where Store.Close() is not explicitly called)

Testing:
- Added singleton_test.go with comprehensive tests
- Fixed goroutine leak in concurrent test by closing old instance
  before resetting singleton state
- Tests verify single instance creation
- Tests verify thread-safe concurrent access
- Tests verify functionality persistence across references

Signed-off-by: penfree <penfree.qiu@gmail.com>
Change-Id: I4261b75ca31f33465a51f7fd7aa6bba7c5435de1
Signed-off-by: penfree <penfree.qiu@gmail.com>
@penfree penfree force-pushed the bugfix/syncindexer branch from a4c700c to 5d6a8a0 Compare April 11, 2026 03:23
@penfree penfree marked this pull request as ready for review April 11, 2026 03:35
@varungup90 varungup90 merged commit 38f690a into vllm-project:main Apr 11, 2026
17 of 18 checks passed
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.

2 participants