diyclassics Claude Opus 4.8 (1M context) commited on
Commit
b949170
·
1 Parent(s): 67d78de

perf: shared cached model + background preload at home page

Browse files

The parsing demo called spacy.load() directly on every rerun (the
@st .cache_resource loader beside it was dead code), so it reloaded the
~500MB lg model on every interaction — the main cause of its slowness.
The custom-label demo had the same uncached-load-per-rerun pattern.

Changes:
- Add model_helpers.py: one @st .cache_resource load_model() shared by all
read-only demos, so they reuse a single in-memory model instead of each
page caching its own copy. start_preload() warms the default lg model in
a daemon thread from app.py, so the model loads while the visitor reads
the home page and the first demo opened finds it already resident.
- Parsing/senter/NER/dependency demos now use the shared loader; drop their
per-page loaders and now-unused imports (spacy, gc, subprocess, json,
registry, and the dead load_model_in_subprocess helper).
- Custom-label demo keeps its OWN cached instance (it mutates the pipeline
via add_pipe("dcc_core")); sharing would leak that pipe into other demos.
Its load is now cached with an idempotent add_pipe guard.

Smoke-tested: shared loader returns a single instance, preload is
non-blocking, trf_vectors disabled, parse works.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

app.py CHANGED
@@ -1,10 +1,17 @@
1
  import streamlit as st
2
 
 
 
3
  st.set_page_config(
4
  page_title="LatinCy Dashboard | Home",
5
  page_icon="🏠",
6
  )
7
 
 
 
 
 
 
8
  st.write("# LatinCy Dashboard")
9
 
10
  st.sidebar.success("Select a demo above.")
 
1
  import streamlit as st
2
 
3
+ from model_helpers import start_preload
4
+
5
  st.set_page_config(
6
  page_title="LatinCy Dashboard | Home",
7
  page_icon="🏠",
8
  )
9
 
10
+ # Warm the default lg model in a background thread while the visitor reads this
11
+ # page, so the first demo they open finds it already loaded. Non-blocking and
12
+ # idempotent — safe to call on every render.
13
+ start_preload()
14
+
15
  st.write("# LatinCy Dashboard")
16
 
17
  st.sidebar.success("Select a demo above.")
model_helpers.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shared spaCy model loading + background preload for the dashboard.
2
+
3
+ Read-only demos import ``load_model()`` from here so they all share ONE model
4
+ instance per name across the whole process, instead of each page caching its
5
+ own copy (which multiplied a ~500 MB model across pages in RAM).
6
+ ``start_preload()`` warms the default model in a background thread from the
7
+ home page, so navigating into a demo finds the model already resident.
8
+
9
+ Demos that MUTATE their pipeline (e.g. the custom-label demo, which adds a
10
+ component with ``add_pipe``) must NOT use this shared instance — they should
11
+ cache their own copy — because mutating the shared model would leak the change
12
+ into every other demo.
13
+ """
14
+
15
+ import threading
16
+
17
+ import spacy
18
+ import streamlit as st
19
+
20
+ DEFAULT_MODEL = "la_core_web_lg"
21
+
22
+ # Single source of truth for loaded models, shared between the background
23
+ # preloader and the synchronous loader. Guarded by _lock so a preload and a
24
+ # concurrent load_model() can never start two loads of the same model.
25
+ _models = {}
26
+ _lock = threading.Lock()
27
+
28
+
29
+ def _load(model_name):
30
+ """Load a model with no Streamlit (st.*) calls — safe off the main thread."""
31
+ nlp = spacy.load(model_name)
32
+ if "trf_vectors" in nlp.pipe_names: # heavy, unused in these demos
33
+ nlp.disable_pipe("trf_vectors")
34
+ return nlp
35
+
36
+
37
+ def _get(model_name):
38
+ with _lock:
39
+ if model_name not in _models:
40
+ _models[model_name] = _load(model_name)
41
+ return _models[model_name]
42
+
43
+
44
+ def start_preload(model_name=DEFAULT_MODEL):
45
+ """Kick off a non-blocking background load of the default model.
46
+
47
+ Called from the home page so the model warms while the user reads the demo
48
+ list. Cheap to call repeatedly: once loaded, the worker returns at once.
49
+ """
50
+ if model_name in _models:
51
+ return
52
+ threading.Thread(target=_get, args=(model_name,), daemon=True).start()
53
+
54
+
55
+ @st.cache_resource(show_spinner="Loading LatinCy model…")
56
+ def load_model(model_name=DEFAULT_MODEL):
57
+ """Return the shared, cached spaCy model for ``model_name``.
58
+
59
+ Reuses the background-preloaded instance when ready; otherwise loads it once
60
+ synchronously. Read-only use only — do not ``add_pipe``/``disable`` on the
61
+ result, since every demo shares this instance.
62
+ """
63
+ return _get(model_name)
pages/1_parsing_demo.py CHANGED
@@ -1,11 +1,8 @@
1
  import streamlit as st
2
- import spacy
3
  import pandas as pd
4
  import datetime
5
- import gc
6
- import subprocess
7
- import json
8
- from spacy.util import registry
9
 
10
  st.set_page_config(page_title="Parsing Demo", layout="wide")
11
  st.sidebar.header("Parsing Demo")
@@ -73,41 +70,12 @@ def analyze_text(text):
73
 
74
  st.title("LatinCy Text Analyzer")
75
 
76
- # Using object notation
77
- first_model = "la_core_web_lg"
78
- first_model_version = spacy.info(first_model)["version"]
79
-
80
-
81
- # Function to unload the current model
82
- @st.cache_resource
83
- def load_model(model_name):
84
- gc.collect() # Attempt to free memory
85
- nlp = spacy.load(model_name)
86
- try:
87
- if "trf_vectors" in nlp.pipe_names: # Disable trf_vectors if it exists
88
- nlp.disable_pipe("trf_vectors")
89
- except Exception as e:
90
- st.warning(f"Failed to disable 'trf_vectors': {e}")
91
- return nlp
92
-
93
-
94
- # Function to load model in a subprocess to avoid conflicts
95
- def load_model_in_subprocess(model_name):
96
- script = (
97
- "import spacy, json; "
98
- 'nlp = spacy.load("' + model_name + '"); '
99
- "print(json.dumps(nlp.meta))"
100
- )
101
- result = subprocess.run(["python", "-c", script], capture_output=True, text=True)
102
- if result.returncode != 0:
103
- raise RuntimeError(f"Failed to load model {model_name}: {result.stderr}")
104
- return json.loads(result.stdout)
105
-
106
-
107
- model_name = "la_core_web_lg" # Hardcoded to use only the lg model
108
- nlp = spacy.load(model_name) # Directly load the lg model
109
 
110
- st.write(f"Loaded model: {model_name} (v{spacy.info(model_name)['version']})")
111
 
112
  df = None
113
 
 
1
  import streamlit as st
 
2
  import pandas as pd
3
  import datetime
4
+
5
+ from model_helpers import load_model
 
 
6
 
7
  st.set_page_config(page_title="Parsing Demo", layout="wide")
8
  st.sidebar.header("Parsing Demo")
 
70
 
71
  st.title("LatinCy Text Analyzer")
72
 
73
+ # Shared, cached lg model — warmed at startup by the home-page preloader, so
74
+ # this returns instantly on all but the very first (cold) load.
75
+ model_name = "la_core_web_lg"
76
+ nlp = load_model(model_name)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
 
78
+ st.write(f"Loaded model: {model_name} (v{nlp.meta['version']})")
79
 
80
  df = None
81
 
pages/2_custom_label_demo.py CHANGED
@@ -46,10 +46,19 @@ model_selectbox = st.sidebar.selectbox(
46
  ("la_core_web_lg", "la_core_web_md", "la_core_web_sm")
47
  )
48
 
49
- nlp = spacy.load(model_selectbox)
50
-
51
- # Add component to pipeline
52
- nlp.add_pipe("dcc_core", last=True)
 
 
 
 
 
 
 
 
 
53
 
54
  tab1, tab2 = st.tabs(["Analyze", "About"])
55
 
 
46
  ("la_core_web_lg", "la_core_web_md", "la_core_web_sm")
47
  )
48
 
49
+ # This demo mutates its pipeline (adds the dcc_core component), so it caches
50
+ # its OWN instance rather than sharing the read-only model from model_helpers —
51
+ # mutating a shared model would leak dcc_core into the other demos. The guard
52
+ # keeps add_pipe idempotent across Streamlit reruns of the cached instance.
53
+ @st.cache_resource
54
+ def load_dcc_model(model_name):
55
+ nlp = spacy.load(model_name)
56
+ if "dcc_core" not in nlp.pipe_names:
57
+ nlp.add_pipe("dcc_core", last=True)
58
+ return nlp
59
+
60
+
61
+ nlp = load_dcc_model(model_selectbox)
62
 
63
  tab1, tab2 = st.tabs(["Analyze", "About"])
64
 
pages/3_senter_demo.py CHANGED
@@ -1,17 +1,13 @@
1
  import streamlit as st
2
- import spacy
3
  import datetime
4
 
 
 
5
  st.set_page_config(page_title="Sentence Segmenter", layout="wide")
6
  st.sidebar.header("Sentence Segmenter")
7
 
8
 
9
- # Load spaCy model (Latin large)
10
- @st.cache_resource
11
- def load_model():
12
- return spacy.load("la_core_web_lg")
13
-
14
-
15
  nlp = load_model()
16
 
17
  st.title("Latin Sentence Segmenter")
 
1
  import streamlit as st
 
2
  import datetime
3
 
4
+ from model_helpers import load_model
5
+
6
  st.set_page_config(page_title="Sentence Segmenter", layout="wide")
7
  st.sidebar.header("Sentence Segmenter")
8
 
9
 
10
+ # Shared, cached lg model (warmed by the home-page preloader)
 
 
 
 
 
11
  nlp = load_model()
12
 
13
  st.title("Latin Sentence Segmenter")
pages/4_ner_demo.py CHANGED
@@ -1,7 +1,8 @@
1
  import streamlit as st
2
- import spacy
3
  from spacy_streamlit import visualize_ner
4
 
 
 
5
  st.set_page_config(page_title="NER Demo", layout="wide")
6
  st.sidebar.header("NER Demo")
7
 
@@ -23,11 +24,6 @@ model_selectbox = st.sidebar.selectbox(
23
  )
24
 
25
 
26
- @st.cache_resource
27
- def load_model(model_name):
28
- return spacy.load(model_name)
29
-
30
-
31
  nlp = load_model(model_selectbox)
32
 
33
  tab1, tab2 = st.tabs(["Recognize", "About"])
 
1
  import streamlit as st
 
2
  from spacy_streamlit import visualize_ner
3
 
4
+ from model_helpers import load_model
5
+
6
  st.set_page_config(page_title="NER Demo", layout="wide")
7
  st.sidebar.header("NER Demo")
8
 
 
24
  )
25
 
26
 
 
 
 
 
 
27
  nlp = load_model(model_selectbox)
28
 
29
  tab1, tab2 = st.tabs(["Recognize", "About"])
pages/5_dependency_demo.py CHANGED
@@ -1,7 +1,8 @@
1
  import streamlit as st
2
- import spacy
3
  from spacy_streamlit import visualize_parser
4
 
 
 
5
  st.set_page_config(page_title="Dependency Demo", layout="wide")
6
  st.sidebar.header("Dependency Demo")
7
 
@@ -32,11 +33,6 @@ model_selectbox = st.sidebar.selectbox(
32
  compact = st.sidebar.checkbox("Compact mode", value=False)
33
 
34
 
35
- @st.cache_resource
36
- def load_model(model_name):
37
- return spacy.load(model_name)
38
-
39
-
40
  nlp = load_model(model_selectbox)
41
 
42
  tab1, tab2 = st.tabs(["Parse", "About"])
 
1
  import streamlit as st
 
2
  from spacy_streamlit import visualize_parser
3
 
4
+ from model_helpers import load_model
5
+
6
  st.set_page_config(page_title="Dependency Demo", layout="wide")
7
  st.sidebar.header("Dependency Demo")
8
 
 
33
  compact = st.sidebar.checkbox("Compact mode", value=False)
34
 
35
 
 
 
 
 
 
36
  nlp = load_model(model_selectbox)
37
 
38
  tab1, tab2 = st.tabs(["Parse", "About"])