Pliploop commited on
Commit
cbfe92b
·
verified ·
1 Parent(s): 4ef8850

Upload folder using huggingface_hub

Browse files
Files changed (3) hide show
  1. README.md +3 -2
  2. app.py +3 -4
  3. core.py +64 -18
README.md CHANGED
@@ -19,8 +19,9 @@ sliders together. The query embedding is edited through sparse inversion in a
19
  trained BatchTopK SAE (MuQ-MuLan / music4all), and nearest neighbours are retrieved
20
  live after each slider move. No audio is hosted — playback is via Spotify embeds.
21
 
22
- Runs on **ZeroGPU**: the corpus + MuQ weights are fetched at startup; the model is built
23
- on the GPU lazily inside the first request and cached.
 
24
 
25
  ## Configuration (Space variables / secrets)
26
 
 
19
  trained BatchTopK SAE (MuQ-MuLan / music4all), and nearest neighbours are retrieved
20
  live after each slider move. No audio is hosted — playback is via Spotify embeds.
21
 
22
+ Runs on **ZeroGPU**: the corpus + MuQ weights are fetched at startup; the text tower is
23
+ used on GPU only while a new slider is instantiated. Sparse inversion, slider edits,
24
+ and retrieval run on CPU from cached slider masks.
25
 
26
  ## Configuration (Space variables / secrets)
27
 
app.py CHANGED
@@ -4,9 +4,9 @@ Search for a seed track or draw one at random, make one or more concept sliders,
4
  and retrieve live as the sliders move. No audio is hosted — playback is via
5
  Spotify embeds.
6
 
7
- ZeroGPU: the corpus + MuQ weights are fetched at startup (CPU); the model is built
8
- on the GPU lazily inside the first @spaces.GPU call and cached for subsequent
9
- requests.
10
  """
11
  import html
12
  import os
@@ -259,7 +259,6 @@ def make_slider(seed_track_id, concept, concepts, *alphas):
259
  return concepts, note, results_html(results), *_slider_components(concepts, values)
260
 
261
 
262
- @spaces.GPU(duration=120)
263
  def live_retrieve(seed_track_id, concepts, *alphas):
264
  concepts = list(concepts or [])
265
  if not seed_track_id:
 
4
  and retrieve live as the sliders move. No audio is hosted — playback is via
5
  Spotify embeds.
6
 
7
+ ZeroGPU: the corpus + MuQ weights are fetched at startup (CPU); the text tower is
8
+ used on GPU only while a new slider is made. Live slider edits reuse cached CPU
9
+ masks.
10
  """
11
  import html
12
  import os
 
259
  return concepts, note, results_html(results), *_slider_components(concepts, values)
260
 
261
 
 
262
  def live_retrieve(seed_track_id, concepts, *alphas):
263
  concepts = list(concepts or [])
264
  if not seed_track_id:
core.py CHANGED
@@ -1,9 +1,9 @@
1
  """Shared engine for the demo Space and website example generation.
2
 
3
  Loads a trained BatchTopK SAE + MuQ text tower and a music4all corpus, then steers a
4
- seed track toward a free-form concept and retrieves nearest neighbours. Corpus load
5
- (CPU, cheap) is separated from model load (GPU, MuQ) so the Space can fetch the corpus at
6
- startup and build the model lazily inside a ZeroGPU call.
7
 
8
  Configuration (env):
9
  SSR_CHECKPOINT local .ckpt path, or a HF model repo id (default: local L0=20 run)
@@ -16,14 +16,13 @@ from __future__ import annotations
16
 
17
  import json
18
  import os
19
- from functools import lru_cache
20
 
21
  import numpy as np
22
  import torch
 
23
 
24
  from steerable_retrieval.steer import Slider
25
  from steerable_retrieval.steer.loading import load_steerable_sae
26
- from steerable_retrieval.steer.retrieval import topk_cosine_neighbors
27
  from steerable_retrieval.steer.steering import decode_normalized
28
 
29
  HERE = os.path.dirname(__file__)
@@ -32,6 +31,7 @@ CORPUS_REPO = os.environ.get("SSR_CORPUS_REPO") # HF dataset repo id, if hosted
32
  CHECKPOINT = os.environ.get("SSR_CHECKPOINT", os.path.join(HERE, os.pardir, "logs/xps/66a0caf9/checkpoints/last.ckpt"))
33
  L0_SUBFOLDER = os.environ.get("SSR_L0") # e.g. "L0-20" when CHECKPOINT is a Hub repo
34
  CONFIG = os.environ.get("SSR_CONFIG")
 
35
 
36
 
37
  def device() -> str:
@@ -57,17 +57,49 @@ def load_corpus():
57
 
58
  class DemoEngine:
59
  def __init__(self, corpus=None, dev: str | None = None):
60
- self.device = dev or device()
 
 
 
61
  embs, ids, meta = corpus if corpus is not None else load_corpus()
62
- self.embeddings = torch.from_numpy(embs).to(self.device)
 
63
  self.track_ids = ids
64
  self.id_to_idx = {t: i for i, t in enumerate(ids)}
65
  self.meta = meta
66
- self.model = load_steerable_sae(CHECKPOINT, device=self.device, subfolder=L0_SUBFOLDER, config_path=CONFIG)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
 
68
- @lru_cache(maxsize=64)
69
  def _slider(self, concept: str) -> Slider:
70
- return Slider(concept, model=self.model, method="adam")
 
 
 
 
 
 
 
 
 
 
71
 
72
  def track_meta(self, track_id: str) -> dict:
73
  m = dict(self.meta.get(track_id, {}))
@@ -77,11 +109,21 @@ class DemoEngine:
77
  def seed_embedding(self, track_id: str) -> torch.Tensor:
78
  return self.embeddings[self.id_to_idx[track_id]]
79
 
 
 
 
 
 
 
 
 
 
80
  def steer_and_retrieve(self, seed_track_id: str, concept: str, alpha: float = 1.0, k: int = 8) -> list[dict]:
81
  slider = self._slider(concept)
82
  z = self.seed_embedding(seed_track_id)
83
  seed_idx = self.id_to_idx[seed_track_id]
84
- idx, scores = slider.retrieve(z, self.embeddings, alpha=alpha, k=k, exclude_idx=seed_idx)
 
85
  return self._format_results(idx, scores)
86
 
87
  def multi_steer_and_retrieve(self, seed_track_id: str, sliders: list[tuple[str, float]], k: int = 8) -> list[dict]:
@@ -93,19 +135,23 @@ class DemoEngine:
93
  """
94
  z = self.seed_embedding(seed_track_id).reshape(1, -1)
95
  seed_idx = self.id_to_idx[seed_track_id]
 
 
 
 
 
 
 
 
96
  with torch.inference_mode():
97
  _, sparse, _, _ = self.model.inference(z)
98
  edited_sparse = sparse.clone()
99
- for concept, alpha in sliders:
100
- concept = concept.strip()
101
- alpha = float(alpha)
102
- if not concept or abs(alpha) < 1e-6:
103
- continue
104
- mask = self._slider(concept).mask.to(device=self.device, dtype=edited_sparse.dtype)
105
  edited_sparse = edited_sparse + alpha * mask.reshape(1, -1)
106
  edited_sparse = edited_sparse.clamp_min(0.0)
107
  edited = decode_normalized(self.model, edited_sparse)
108
- idx, scores = topk_cosine_neighbors(edited, self.embeddings, k, exclude_idx=seed_idx)
109
  return self._format_results(idx, scores)
110
 
111
  def _format_results(self, idx, scores) -> list[dict]:
 
1
  """Shared engine for the demo Space and website example generation.
2
 
3
  Loads a trained BatchTopK SAE + MuQ text tower and a music4all corpus, then steers a
4
+ seed track toward free-form concepts and retrieves nearest neighbours. The SAE,
5
+ inversion, edit masks, and retrieval corpus stay on CPU; ZeroGPU is used only while
6
+ embedding a new text concept for slider instantiation.
7
 
8
  Configuration (env):
9
  SSR_CHECKPOINT local .ckpt path, or a HF model repo id (default: local L0=20 run)
 
16
 
17
  import json
18
  import os
 
19
 
20
  import numpy as np
21
  import torch
22
+ import torch.nn.functional as F
23
 
24
  from steerable_retrieval.steer import Slider
25
  from steerable_retrieval.steer.loading import load_steerable_sae
 
26
  from steerable_retrieval.steer.steering import decode_normalized
27
 
28
  HERE = os.path.dirname(__file__)
 
31
  CHECKPOINT = os.environ.get("SSR_CHECKPOINT", os.path.join(HERE, os.pardir, "logs/xps/66a0caf9/checkpoints/last.ckpt"))
32
  L0_SUBFOLDER = os.environ.get("SSR_L0") # e.g. "L0-20" when CHECKPOINT is a Hub repo
33
  CONFIG = os.environ.get("SSR_CONFIG")
34
+ INVERSION_METHOD = os.environ.get("SSR_INVERSION_METHOD", "fista")
35
 
36
 
37
  def device() -> str:
 
57
 
58
  class DemoEngine:
59
  def __init__(self, corpus=None, dev: str | None = None):
60
+ # Keep SAE inversion, sparse edits, and retrieval on CPU. ZeroGPU is only
61
+ # needed while embedding new text concepts with MuQ.
62
+ self.device = "cpu"
63
+ self.text_device = dev or device()
64
  embs, ids, meta = corpus if corpus is not None else load_corpus()
65
+ self.embeddings = torch.from_numpy(embs).float()
66
+ self.gallery = F.normalize(self.embeddings, dim=-1)
67
  self.track_ids = ids
68
  self.id_to_idx = {t: i for i, t in enumerate(ids)}
69
  self.meta = meta
70
+ self.model = load_steerable_sae(CHECKPOINT, device="cpu", subfolder=L0_SUBFOLDER, config_path=CONFIG)
71
+ self._slider_cache: dict[str, Slider] = {}
72
+ self._text_encoder_device = "cpu"
73
+
74
+ def _slider_key(self, concept: str) -> str:
75
+ return " ".join(concept.strip().lower().split())
76
+
77
+ def has_slider(self, concept: str) -> bool:
78
+ return self._slider_key(concept) in self._slider_cache
79
+
80
+ def _set_text_encoder_device(self, target: str) -> None:
81
+ if self._text_encoder_device == target:
82
+ return
83
+ text_encoder = self.model.text_encoder
84
+ if text_encoder is None:
85
+ return
86
+ text_encoder.to(target)
87
+ if hasattr(text_encoder, "device"):
88
+ text_encoder.device = target
89
+ self._text_encoder_device = target
90
 
 
91
  def _slider(self, concept: str) -> Slider:
92
+ key = self._slider_key(concept)
93
+ if key not in self._slider_cache:
94
+ target = self.text_device if str(self.text_device).startswith("cuda") and torch.cuda.is_available() else "cpu"
95
+ self._set_text_encoder_device(target)
96
+ try:
97
+ self._slider_cache[key] = Slider(concept.strip(), model=self.model, method=INVERSION_METHOD, device="cpu")
98
+ finally:
99
+ # The cached slider stores its text embedding/mask; live edits no
100
+ # longer need the text tower or any GPU memory.
101
+ self._set_text_encoder_device("cpu")
102
+ return self._slider_cache[key]
103
 
104
  def track_meta(self, track_id: str) -> dict:
105
  m = dict(self.meta.get(track_id, {}))
 
109
  def seed_embedding(self, track_id: str) -> torch.Tensor:
110
  return self.embeddings[self.id_to_idx[track_id]]
111
 
112
+ def _retrieve_dense(self, query: torch.Tensor, k: int, *, exclude_idx: int | None = None):
113
+ query = F.normalize(query.reshape(-1), dim=0)
114
+ sims = self.gallery @ query
115
+ if exclude_idx is not None:
116
+ sims[exclude_idx] = -1e9
117
+ n_avail = self.gallery.shape[0] - (1 if exclude_idx is not None else 0)
118
+ vals, idx = torch.topk(sims, k=min(int(k), int(n_avail)), largest=True, sorted=True)
119
+ return idx.detach().cpu(), vals.detach().cpu()
120
+
121
  def steer_and_retrieve(self, seed_track_id: str, concept: str, alpha: float = 1.0, k: int = 8) -> list[dict]:
122
  slider = self._slider(concept)
123
  z = self.seed_embedding(seed_track_id)
124
  seed_idx = self.id_to_idx[seed_track_id]
125
+ edited = slider.steer(z, alpha=alpha)
126
+ idx, scores = self._retrieve_dense(edited, k, exclude_idx=seed_idx)
127
  return self._format_results(idx, scores)
128
 
129
  def multi_steer_and_retrieve(self, seed_track_id: str, sliders: list[tuple[str, float]], k: int = 8) -> list[dict]:
 
135
  """
136
  z = self.seed_embedding(seed_track_id).reshape(1, -1)
137
  seed_idx = self.id_to_idx[seed_track_id]
138
+ slider_masks = []
139
+ for concept, alpha in sliders:
140
+ concept = concept.strip()
141
+ alpha = float(alpha)
142
+ if not concept or abs(alpha) < 1e-6:
143
+ continue
144
+ slider_masks.append((self._slider(concept).mask, alpha))
145
+
146
  with torch.inference_mode():
147
  _, sparse, _, _ = self.model.inference(z)
148
  edited_sparse = sparse.clone()
149
+ for mask, alpha in slider_masks:
150
+ mask = mask.to(device=self.device, dtype=edited_sparse.dtype)
 
 
 
 
151
  edited_sparse = edited_sparse + alpha * mask.reshape(1, -1)
152
  edited_sparse = edited_sparse.clamp_min(0.0)
153
  edited = decode_normalized(self.model, edited_sparse)
154
+ idx, scores = self._retrieve_dense(edited, k, exclude_idx=seed_idx)
155
  return self._format_results(idx, scores)
156
 
157
  def _format_results(self, idx, scores) -> list[dict]: