Skip to content
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

Using keys to preserve values between reloads #8056

Merged
merged 21 commits into from
Apr 25, 2024
Merged

Conversation

aliabid94
Copy link
Collaborator

Added the concept of a key to a Block, such that any Block that has the same key between re-renders has its value preserved between renders. Will be needed specifically for render PR.

Implemented in dev mode by automatically assigning every Block a generated key and giving similarly positioned blocks have the same key across renders. See below how re-renders do not change the value of the components.

BEFORE:
Recording 2024-04-17 at 18 14 02

AFTER:
Recording 2024-04-17 at 18 10 29

@gradio-pr-bot
Copy link
Collaborator

gradio-pr-bot commented Apr 17, 2024

🪼 branch checks and previews

Name Status URL
Spaces ready! Spaces preview
Website ready! Website preview
Storybook ready! Storybook preview
🦄 Changes detected! Details

Install Gradio from this PR

pip install https://gradio-builds.s3.amazonaws.com/fe9003186dce675bcf9fd94a892b9bcfb1fae9f2/gradio-4.27.0-py3-none-any.whl

Install Gradio Python Client from this PR

pip install "gradio-client @ git+https://github.com/gradio-app/gradio@fe9003186dce675bcf9fd94a892b9bcfb1fae9f2#subdirectory=client/python"

@gradio-pr-bot
Copy link
Collaborator

gradio-pr-bot commented Apr 17, 2024

🦄 change detected

This Pull Request includes changes to the following packages.

Package Version
@gradio/app minor
@gradio/client minor
gradio minor
  • Maintainers can select this checkbox to manually select packages to update.

With the following changelog entry.

Using keys to preserve values between reloads

Maintainers or the PR author can modify the PR title to modify this entry.

Something isn't right?

  • Maintainers can change the version label to modify the version bump.
  • If the bot has failed to detect any changes, or if this pull request needs to update multiple packages to different versions or requires a more comprehensive changelog entry, maintainers can update the changelog file directly.

if self.dev_mode:
for block in self.blocks.values():
if block.key is None:
block.key = f"__{block._id}__"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we make the id a hash of the component's config? That way it will be unique across re-renders unless something about the component changed and we don't need to do the reassign_keys logic.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

So what you're suggesting is that we use the hash of the config to decide what components are considered identical across re-renders. While we could use that logic in utils.reassign_keys to consider what components are equivalent, it's gonna be problematic because it's very likely two components have the exact same config, e.g.

with gr.Blocks():
  a = gr.Textbox()
  b = gr.Textbox()

I think it makes more sense to rely on the position and type of components to assign identity. The logic in utils.reassign_keys can always be improved but keep in mind this is only for dev mode refreshes - in actual render blocks, we will require users to explicitly assign keys to maintain identity.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I see your point about duplicate keys but is that a concern for reload mode? Even if auto-generated ids collide, the component fetched in the frontend's restored_keyed_values would be correct since the config should determine the state of the component.

The problem with using the position to determine equivalence is that if you insert a component "in between" or "before" already existing components, then the front-end state would get wiped.

This PR is already an improvement of what we currently do but wondering if we can create a better heuristic for equivalence from the get-go.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Even if auto-generated ids collide, the component fetched in the frontend's restored_keyed_values would be correct since the config should determine the state of the component.

I don't think this is the case. If initially I have:

with gr.Blocks():
    boxA = gr.Textbox()
    boxB = gr.Textbox()

and I type something into boxA, and then I change boxA to:

with gr.Blocks():
    boxA = gr.Textbox(label="A")
    boxB = gr.Textbox()

then the value that was in A would get moved to B, since that's the best match by config logic.

This PR is already an improvement of what we currently do but wondering if we can create a better heuristic for equivalence from the get-go.

Sure, I think we can iterate on reassign_keys logic in other PRs, but since its only used in dev mode I think this can be a decent starting point.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ok sounds good!

Comment on lines 55 to 71
const restore_keyed_values = (): void => {
let component_values_by_key: Record<string | number, ComponentMeta> = {};
old_components.forEach((component) => {
if (component.key) {
component_values_by_key[component.key] = component;
}
});
components.forEach((component) => {
if (component.key) {
const old_component = component_values_by_key[component.key];
if (old_component) {
component.props.value = old_component.props.value;
}
}
});
};

Copy link
Member

Choose a reason for hiding this comment

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

Do you think it would be possible to do all of this in the init module? I think we could expose an update_components function where we could pass in components, and the old / new merging could be handled inside that module with all of the other component updates.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

sure, moved to init.ts

Copy link
Member

Choose a reason for hiding this comment

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

Sorry I didn't mean just move the function but rather the old component state could be tracked in the init logic.

We already have the previous component state. If we added an update method that took the new component / deps / layout, we could easily perform the logic all inside of the update method without needing to track the previous state outside of that module.

This way we could also perhaps avoid some work on every component change and instead only do what is necessary (this may be minimal if anything).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Gotcha, makes sense! Implemented this in #8110 which targets this PR.

@@ -155,7 +148,11 @@ def alert_change(self):
self.change_event.set()

def swap_blocks(self, demo: Blocks):
old_blocks = self.running_app.blocks
super().swap_blocks(demo)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we need to take care of adding max_file_size from the old blocks here as well:

demo.max_file_size = old_blocks.max_file_size

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ok, handled in BaseReloader.swap_blocks

Copy link
Collaborator

@freddyaboulton freddyaboulton left a comment

Choose a reason for hiding this comment

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

Thanks for the great PR @aliabid94 !

One remaining issue I noticed is that this breaks custom component reload mode. I created a new component in js/preview/test (needs to be here until the next release because of #8066) and ran dev mode. Changes in the python file were not reflected in the UI. Confirmed it does work on main though.

@pngwn
Copy link
Member

pngwn commented Apr 24, 2024

I can also reproduce what @freddyaboulton is seeing.

@aliabid94
Copy link
Collaborator Author

Okay I believe I fixed the maximum 6 refreshes bug and also the bug @freddyaboulton mentioned by simply closing the heartbeat connection on refreshes. Should be ready!

Copy link
Collaborator

@freddyaboulton freddyaboulton left a comment

Choose a reason for hiding this comment

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

Tested and works well @aliabid94 🔥 Nice

pnpm-lock.yaml Outdated
@@ -1478,6 +1478,25 @@ importers:
specifier: ^3.28.0
version: 3.28.0

js/preview/test/mycomponent/frontend:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Revert this change please

@abidlabs
Copy link
Member

abidlabs commented Apr 25, 2024

Didn't do a deep review but it looks like the issues that I noticed are fixed. Nice!!

As discussed previously, would be cool to talk about how values are preserved:

@aliabid94 aliabid94 merged commit 2e469a5 into main Apr 25, 2024
7 of 8 checks passed
@aliabid94 aliabid94 deleted the assign_keys_to_reload branch April 25, 2024 18:25
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.

5 participants