-
Notifications
You must be signed in to change notification settings - Fork 955
Adding support for sharing memory between the module and the engine #2472
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: unstable
Are you sure you want to change the base?
Conversation
Signed-off-by: yairgott <[email protected]>
Signed-off-by: yairgott <[email protected]>
Signed-off-by: yairgott <[email protected]>
Signed-off-by: yairgott <[email protected]>
Signed-off-by: yairgott <[email protected]>
ranshid
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@yairgott placing some high level comments first:
- Not sure I like the ViewValue naming. I think the intention is to keep a "string" pointer and a length right? in that case maybe we should simply do that and call it a
StringValue? - I did not understand why we have to change the
entryGetValueAPI? I would prefer to keep a separate API to get the "string" value likeentryGetStringValue(or in your caseentryGetViewValue)
Signed-off-by: yairgott <[email protected]>
Renamed it to bufferView.
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## unstable #2472 +/- ##
=========================================
Coverage 72.41% 72.41%
=========================================
Files 128 128
Lines 70414 70501 +87
=========================================
+ Hits 50987 51054 +67
- Misses 19427 19447 +20
🚀 New features to boost your workflow:
|
|
I am still providing high level comments (although I do have detailed comments) since IMO we should solve the highlevel first. I think we should have a clear definition of the entry API. Currently the entry is defined as a storage for sds type field and sds type value. In your suggestion this is changed and an entry can be provided with a value which is either an sds or a pointer to a native string and will INTERNALLY encode it the way it would like to. Lets list the reason for this change IIUC:
Your suggestion is to change the entry API so:
I think that following this suggestion I would handle the following cases:
I also think the top comment might be missing some more alternatives that we consider(?):
|
Fixed
I'm not 100% clear but I'll note that outside of entry.c, one should still be using the public interface
entryCreate - creating an entry with a value view is not supported. An entry may switch to store a value view by calling t_hash.c, hashTypeSetValueView. For safety, I've incorporated a debug mode verification that the provided view buffer matches the entry SDS.
Naming is hard :) The name
|
entryGetValueRef, is meant for internal use and will not work in all cases, so it is not fit for a public API. if, for example I am a user of entry and I provided an entry with sds and I need to continue using an sds from that entry, I have no way of doing so aside for creating a new sds.
From reading the code I see that:
I think this needs to be clear. If the entry is allowing to change an entry which is created with an sds value to an entry which has a "view", why we cannot do the opposite? and why do we expose a public API that is simply asserting instead of preventing this in some way? I think that as the entry is a stand-alone module, it should be generic and flexible, or the API should be restrictive and not allow what is simply not supported.
Well we are not doing c++ unfortunately, and pointers here are treated as references IMO. So I think it is a better name, but will not make this the main point of the review. I just think that the way to distinguish a "native c style string" to what you have which requires to ALWAYS keep the provided reference is to use a name like stringref. To conclude. I would like to have a complete API which is both generic and standalone together with supportive to the existing usecases we have. As I mentioned before, we could allow entry to accept both sds value and "native c style strings" and encode it internally which is subject to the internal implementation which should prefer memory efficiency and performance (avoid large copies and better cache line locality). For first thing we should solve the part of the API which accepts values if we plan to NOT allow accepting . How do we handle the |
Right, I'll fix this. The idea is to handle adding expiry to a view value. Otherwise, the existing code, with slight changes, will handle updating a view value.
I'm not religious about the name, if you feel strongly about the
I think that making the API complete is not really an objective but rather supporting the use-cases. Take for example
Any update to the entry is handled by a call to |
I think that the entry should keep a clear and concrete API. this API is not going to be used ONLY by the search module, but potentially by other future parts of the application as well as future modules, and it would be great if we could make the API complete. But let me try and track it better in the detailed review.
So that would require to handle entryUpdate correctly. I see that you suggest you will fix that, so I will followup on that. |
|
@yairgott also please make a run through the current documentation and structure ascii and change them accordingly. |
Signed-off-by: yairgott <[email protected]>
Signed-off-by: yairgott <[email protected]>
Signed-off-by: yairgott <[email protected]>
entryUpdate already works correctly. Let me know if you find any issues. I've also added unittests.
Sure. Also, noting that I've done the renaming to stringRef. |
Signed-off-by: yairgott <[email protected]>
Signed-off-by: yairgott <[email protected]>
ranshid
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
went though some more stuff. will continue tomorrow
src/entry.c
Outdated
| return entryWrite(buf, buf_size, field, value, expiry, embed_value, embedded_field_sds_type, embedded_field_sds_size, embedded_value_sds_size, expiry_size); | ||
| } | ||
|
|
||
| entry *entrySetStringRef(entry *entry, const char *buf, size_t len, long long expiry) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To me it sounds like we are duplicating some stuff form entryUpdate. The way I imagined it is that the entry can be encoded either like:
1. field | embedded value
2. value ptr | field
3. ttl | field | embedded value
4. ttl | value ptr | field
5. vstring | vlen | field
6. ttl | vstring | vlen | field
So entryUpdate should be able to navigate between ALL these cases and as such can be used inside entrySetStringRef.
I also still not a big fan of the fact that there is no real way to create an entry with a stringRef. Maybe it is not needed right now, but the API seems strange that way IMO.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Decoupling with a dedicated function, rather than extending entryUpdate to support string ref, is much more straight forward both to implement and to maintain. Extending entryUpdate would involve:
- Supporting both types of input values, sds and [char * buf, size_t len].
- Extending the logic to properly handle all the possible combinations.
In general, we should strive for code which is easy to maintain and resilient. IMO entryUpdate is already too complicated and it already handles too many different cases which adds complexity to follow and to reason about.
In term of code reuse, I will try to explore how to improve the code section in lines 324-342.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Edited!
I would like to suggest the following changes to improve the code quality:
- Starting with
entryUpdate:
entry *entryUpdate(entry *e, sds value, long long expiry) {
if (entryChangeLayout(e, value, expiry)) {
entry *new_entry = entryCreate((sds)e, value, expiry);
entryFree(e);
return new_entry;
}
// Layout was not changed, just apply the value and the expiry
entryReplaceValue(e, value);
if (expiry != EXPIRY_NONE) entryReplaceExpiry(e, expiry);
return e;
}
- Now that
entryReqSizeis called just byentryCreateand therefor its logic can be embedded insideentryCreateand refactored to greatly simplified.
If agreed with the above, I can drive this changes but I prefer to drive this after this PR is landed. WDYT?
|
We discussed this in the core team meeting today. If we've closed on the design and it's been reviewed by next Monday, we can merge it to 9.0 RC 2, but otherwise we can postpone it to 9.1. |
Signed-off-by: Yair Gottdenker <[email protected]>
Signed-off-by: Yair Gottdenker <[email protected]>
Signed-off-by: Yair Gottdenker <[email protected]>
Signed-off-by: Yair Gottdenker <[email protected]>
Signed-off-by: Yair Gottdenker <[email protected]>
|
Overall the code changes LGTM. NOTE: this might have future conflicts with #2618 @valkey-io/core-team how can we progress the major-decision-pending checkout? does any other maintainer wish to go over and check this? |
|
@ranshid Hey, what specifically do you want the core team to address? |
|
We reviewed the APIs, they seem minimal but there are no concerns with it. @JimB123 since Ran asked for another pair of eyes, PTAL. Also Ran, please approve if you are happy with the PR. |
Ack. Reviewing. |
ranshid
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me try and sum-up my thoughts about the status:
Documentation improvements
https://github.com/valkey-io/valkey/pull/2472/files#r2473588727
https://github.com/valkey-io/valkey/pull/2472/files#r2471218383
https://github.com/valkey-io/valkey/pull/2472/files#r2473736680
https://github.com/valkey-io/valkey/pull/2472/files#r2470424309
https://github.com/valkey-io/valkey/pull/2472/files#r2470568137
https://github.com/valkey-io/valkey/pull/2472/files#r2470572782
https://github.com/valkey-io/valkey/pull/2472/files#r2470572981
code refactor
create a dedicated scan type: https://github.com/valkey-io/valkey/pull/2472/files#r2479301622
add a dedicated stringRef getter: https://github.com/valkey-io/valkey/pull/2472/files#r2471108477
refactor entryGetValue: https://github.com/valkey-io/valkey/pull/2472/files#r2471145583
entryUpdate refactor: https://github.com/valkey-io/valkey/pull/2472/files#r2474627473 + https://github.com/valkey-io/valkey/pull/2472/files#r2478426332
https://github.com/valkey-io/valkey/pull/2472/files#r2474655831
entryUpdateAsStringRef: https://github.com/valkey-io/valkey/pull/2472/files#r2474037834 + https://github.com/valkey-io/valkey/pull/2472/files#r2478563045 + https://github.com/valkey-io/valkey/pull/2472/files#r2473574601 + https://github.com/valkey-io/valkey/pull/2472/files#r2479164080+ https://github.com/valkey-io/valkey/pull/2472/files#r2479155661
refactor entryConstruct: https://github.com/valkey-io/valkey/pull/2472/files#r2475553245
trivial code refactor
https://github.com/valkey-io/valkey/pull/2472/files#r2471200320
https://github.com/valkey-io/valkey/pull/2472/files#r2471192263
https://github.com/valkey-io/valkey/pull/2472/files#r2479118890
https://github.com/valkey-io/valkey/pull/2472/files#r2479166951
https://github.com/valkey-io/valkey/pull/2472/files#r2479190818
https://github.com/valkey-io/valkey/pull/2472/files#r2479214355
correctness
https://github.com/valkey-io/valkey/pull/2472/files#r2471130746
https://github.com/valkey-io/valkey/pull/2472/files#r2478894678
https://github.com/valkey-io/valkey/pull/2472/files#r2478794808
https://github.com/valkey-io/valkey/pull/2472/files#r2478831245
https://github.com/valkey-io/valkey/pull/2472/files#r2478899287
open issues
https://github.com/valkey-io/valkey/pull/2472/files#r2473620702
https://github.com/valkey-io/valkey/pull/2472/files#r2475548331 - should be handled when we rebase unstable to solve conflict
https://github.com/valkey-io/valkey/pull/2472/files#r2479323821
IMO the open issues + correctness + trivial code refactor can be completed in order to merge it.
We can consider followup in a different PR about the documentation and major code refactor
@JimB123 @yairgott WDYT?
@ranshid I agree that more significant code refactors (like redesign of Of the items you listed as (non-trivial) code refactors, I think we should do this one now:
|
I agree about the documentation. just would like @yairgott to be aligned |
Signed-off-by: Yair Gottdenker <[email protected]>
| mem += zmalloc_usable_size(entryGetAllocPtr(entry)); | ||
| } | ||
| mem += sdsAllocSize((sds)entryGetValue(entry, NULL)); | ||
| if (!entryHasStringRef(entry)) mem += sdsAllocSize((sds)entryGetValue(entry, NULL)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still feel like we miss accounting for the stringRef (16 bytes) itself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is accounted for in line 457.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@yairgott maybe the lines count shifted with some of the changes.
If I follow the memory count steps, the memory with stringRef dose NOT have an embedded value so I am taking the malloc size of the main entry buffer:
| long long | stringRef (pointer) | sdshdr8+ | "foo" \0 |/ / / / |
Now, that include the pointer to the stringref mid-layer, but we never add this to the count.
| if (!entryHasStringRef(entry)) mem += sdsAllocSize((sds)entryGetValue(entry, NULL)); | |
| if (!entryHasStringRef(entry)) mem += sdsAllocSize((sds)entryGetValue(entry, NULL)); | |
| else mem += sizeof(stringRef); // can also consider mem += zmalloc_usable_size(entryGetStringRefRef(entry)); |
Co-authored-by: Ran Shidlansik <[email protected]> Signed-off-by: Yair Gottdenker <[email protected]>
Signed-off-by: Yair Gottdenker <[email protected]>
Signed-off-by: Yair Gottdenker <[email protected]>
Signed-off-by: Yair Gottdenker <[email protected]>
Signed-off-by: Yair Gottdenker <[email protected]>
Signed-off-by: Yair Gottdenker <[email protected]>
Signed-off-by: Yair Gottdenker <[email protected]>
| if (entryHasValuePtr(entry)) { | ||
| dismissSds(*entryGetValueRef(entry)); | ||
| } | ||
| if (entryHasValuePtr(entry)) entryFreeValuePtr(entry); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we can currently keep the existing logic?
| if (entryHasValuePtr(entry)) entryFreeValuePtr(entry); | |
| if (entryHasValuePtr(entry) && !entryHasStringRef(entry)) dismissSds(*entryGetValueRef(entry)); |
| * +--------------+----------+----------+----------+----------+--------+ | ||
| * | | ||
| * | | ||
| * stringRef value |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the drawing is confusing.
The stringref is another mid-layer part holding the value size and pointer, so maybe we can illustrate that
| mem += zmalloc_usable_size(entryGetAllocPtr(entry)); | ||
| } | ||
| mem += sdsAllocSize((sds)entryGetValue(entry, NULL)); | ||
| if (!entryHasStringRef(entry)) mem += sdsAllocSize((sds)entryGetValue(entry, NULL)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@yairgott maybe the lines count shifted with some of the changes.
If I follow the memory count steps, the memory with stringRef dose NOT have an embedded value so I am taking the malloc size of the main entry buffer:
| long long | stringRef (pointer) | sdshdr8+ | "foo" \0 |/ / / / |
Now, that include the pointer to the stringref mid-layer, but we never add this to the count.
| if (!entryHasStringRef(entry)) mem += sdsAllocSize((sds)entryGetValue(entry, NULL)); | |
| if (!entryHasStringRef(entry)) mem += sdsAllocSize((sds)entryGetValue(entry, NULL)); | |
| else mem += sizeof(stringRef); // can also consider mem += zmalloc_usable_size(entryGetStringRefRef(entry)); |
Overview
Sharing memory between the module and engine reduces memory overhead by eliminating redundant copies of stored records in the module. This is particularly beneficial for search workloads that require indexing large volumes of documents.
Vectors
Vector similarity search requires storing large volumes of high-cardinality vectors. For example, a single vector with 512 dimensions consumes 2048 bytes, and typical workloads often involve millions of vectors. Due to the lack of a memory-sharing mechanism between the module and the engine, valkey-search currently doubles memory consumption when indexing vectors, significantly increasing operational costs. This limitation introduces adoption friction and reduces valkey-search's competitiveness.
Memory Allocation Strategy
At a fundamental level, there are two primary allocation strategies:
For valkey-search, it is crucial that vectors reside in cache-aligned memory to maximize SIMD optimizations. Allowing the module to allocate memory provides greater flexibility for different use cases, though it introduces slightly higher implementation complexity.
Old Implementation
The old implementation was based on ref-counting and introduced a new SDS type. After further discussion, we agreed to simplify the design by removing ref-counting and avoiding the introduction of a new SDS type.
New Implementation - Key Points
VM_HashSetViewValue, which set value as a view of a buffer which is owned by the module. The function accepts the hash key, hash field, and a buffer along with its length.ViewValueis a new data type that captures the externalized buffer and its length.valkey-search Usage
Insertion
VM_HashSetViewValueto avoid keeping two copies of the vector.Deletion
When receiving a key space notification for a deleted hash key or hash field that was indexed as a vector, valkey-search deletes the corresponding entry from the index.
Update
Handled similarly to insertion.