Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/umi-v2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ jobs:
coordinated:
name: UMI v2
# Update the workflow and runner revision together.
uses: KartikP/brainscore-unified/.github/workflows/offline-tests.yml@529237ada8c243cab0ad67cb53abde350342f4ea
uses: KartikP/brainscore-unified/.github/workflows/offline-tests.yml@5dc12f6654f5701b5135c7b288a3327ee704384e
with:
candidate: core
candidate_repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
candidate_revision: ${{ github.event.pull_request.head.sha || github.sha }}
unified_revision: 529237ada8c243cab0ad67cb53abde350342f4ea
unified_revision: 5dc12f6654f5701b5135c7b288a3327ee704384e
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Brain-Score turns experimental data into quantitative benchmarks that evaluate
subjects with compatible inputs, outputs and methods.

> **UMI integration:** `Subject` defines session interaction through
> `interact(session)`. `BrainScoreModel` implements it with extraction and task
> `participate(session)`. `BrainScoreModel` implements it with extraction and task
> helpers, including `process()`. Existing vision `BrainModel` and language
> `ArtificialSubject` plugins remain supported through adapters. See
> [the interface guide](docs/UMI_MIGRATION.md).
Expand Down
4 changes: 2 additions & 2 deletions brainscore_core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from . import nulls

# Warn loudly if the installed dependency versions have drifted off UMI's pins,
# since drift silently breaks/changes scoring (see _env_check for detail).
from ._env_check import warn_on_drift as _warn_on_drift
# since drift silently breaks/changes scoring (see environment_check for detail).
from .environment_check import warn_on_drift as _warn_on_drift
_warn_on_drift()
del _warn_on_drift
124 changes: 6 additions & 118 deletions brainscore_core/_env_check.py
Original file line number Diff line number Diff line change
@@ -1,122 +1,10 @@
"""Runtime check that installed dependencies fall within UMI's compatibility bounds.
"""Former private name of :mod:`brainscore_core.environment_check`.

These are the version ranges scoring is *verified against* — deliberately looser
than any single install pin (that's the environment file's job). Drift outside
them silently breaks scoring. This module warns loudly at import rather than
letting a scored run be silently wrong.

``scikit-learn`` moved from 1.5 to 1.7.2 on 2026-09-10, matching the version
``language/environment_lock.yml`` already locked. The 1.5 pin had made four
registered benchmarks unrunnable — ``Pereira2018.{243,384}sentences-ridge``,
``Blank2014-ridge`` and ``Fedorenko2016-ridge`` all route through
``kfold='group'``, and upstream calls ``GroupKFold(shuffle=..., random_state=...)``,
parameters that only exist from 1.6.

The bump is score-neutral, which was checked rather than assumed:
``LogisticRegression(multi_class='multinomial')`` is still honoured in 1.7.2 and
returns bit-identical probabilities to 1.5.1 for both binary and multiclass
fits. sklearn's own warning says removal was planned for 1.7; it actually
slipped to **1.8**, which is why the upper bound is ``<1.8``. At 1.8 the
argument disappears and binary problems become proper binary logistic fits
(as if ``multi_class='ovr'``), which does move probabilities — measured on a
seeded 400x30 binary fit, mid-range values shift from 0.0019 to 0.0076.
Multiclass is unaffected. Crossing into 1.8 therefore needs the behavioral
readouts re-measured, and is a deliberate step rather than a version bump.

``transformers`` 5 is now supported. The two things that blocked it are handled:
``DynamicCache.to_legacy_cache`` was removed, so the KV sliding window selects a
cache API by shape (``slice_kv_cache``), and the image-processor class names were
rebound so the default moved from PIL to torchvision, which registrations pin
explicitly (``hf_compat.pin_image_processor``). Both were checked against 5.14:
GPT-2 features are bit-identical across the two majors, and the one model whose
processor default actually moved shifts its score by 0.038%.

Uses only ``importlib.metadata`` so ``brainscore_core`` stays free of heavy
dependencies (no torch/transformers import).
The old name is the same module object, not a copy, so code that patches
``_env_check.version`` (as older tests do) still changes what the checks see.
"""
from importlib.metadata import version, PackageNotFoundError

# (distribution name, ok(version_tuple) -> bool, human-readable bound)
_BOUNDS = [
("transformers", lambda v: (4, 57) <= (v[0], v[1]) < (6, 0), ">=4.57,<6"),
("scikit-learn", lambda v: (1, 7) <= (v[0], v[1]) < (1, 8), ">=1.7,<1.8"),
("numpy", lambda v: (1, 21) <= (v[0], v[1]) < (2, 0), ">=1.21,<2"),
("xarray", lambda v: (v[0], v[1], v[2]) == (2022, 3, 0), "==2022.3.0"),
]


def _parse(s):
"""Leading-numeric version tuple, padded to 3 fields. '5.5.0' -> (5, 5, 0)."""
parts = []
for field in s.split(".")[:3]:
num = ""
for ch in field:
if ch.isdigit():
num += ch
else:
break
parts.append(int(num) if num else 0)
while len(parts) < 3:
parts.append(0)
return tuple(parts)


def check_env_bounds():
"""Return human-readable drift messages (empty if all bounds hold)."""
drift = []
for pkg, ok, expected in _BOUNDS:
try:
installed = version(pkg)
except PackageNotFoundError:
continue # package not installed in this (e.g. core-only) env
try:
if not ok(_parse(installed)):
drift.append(
f"{pkg}=={installed} is outside the UMI runtime "
f"compatibility bound ({expected})")
except Exception:
continue # unparseable version string: don't block import
return drift


def warn_on_drift():
"""Emit a RuntimeWarning if any dependency is outside its compatibility bound."""
drift = check_env_bounds()
if drift:
import warnings
warnings.warn(
"Installed dependencies are outside UMI's runtime compatibility "
"bounds (these are the versions scoring is verified against; the exact "
"install pin is the environment file's job):\n - "
+ "\n - ".join(drift)
+ "\nScoring may be SILENTLY WRONG (scikit-learn>=1.8 drops "
"LogisticRegression(multi_class=...), so binary behavioral-readout "
"scores move without erroring). Install the pinned environment "
"(environment-unified.yml) to fix.",
RuntimeWarning,
stacklevel=2,
)


def describe_env():
"""Per-dependency status, including the ones that are fine.
import sys

``check_env_bounds`` reports only problems, which is what a warning needs.
A user asking "is my environment right?" needs to see the passing rows too,
otherwise silence is indistinguishable from nothing having been checked.
from . import environment_check

:return: rows of ``(package, installed or None, expected, ok)`` where ``ok``
is None for a package that is absent or has an unparseable version
"""
rows = []
for pkg, ok, expected in _BOUNDS:
try:
installed = version(pkg)
except PackageNotFoundError:
rows.append((pkg, None, expected, None))
continue
try:
rows.append((pkg, installed, expected, bool(ok(_parse(installed)))))
except Exception:
rows.append((pkg, installed, expected, None))
return rows
sys.modules[__name__] = environment_check
11 changes: 6 additions & 5 deletions brainscore_core/behavioral.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ def start_task(self, task_context_or_task, fitting_stimuli=None,
f"Model '{owner.identifier}' cannot run task "
f"'{task_type}' via generation path "
f"(prefer_path='generation'): "
f"requires model.generation_fn AND TaskContext.instruction "
f"requires an answer function registered with "
f"BrainScoreModel(answer=...) AND TaskContext.instruction "
f"AND TaskContext.label_set to be set."
)
self.use_generation_for_task = True
Expand Down Expand Up @@ -155,8 +156,8 @@ def start_task(self, task_context_or_task, fitting_stimuli=None,
raise ValueError(
f"Model '{owner.identifier}' cannot run behavioral task "
f"'{task_type}': TaskContext provides neither fitting_stimuli "
f"(needed for readout path) nor (instruction + label_set "
f"+ model.generation_fn) (needed for generation path)."
f"(needed for readout path) nor (instruction + label_set, "
f"with BrainScoreModel(answer=...)) (needed for generation path)."
)

def reset(self) -> None:
Expand Down Expand Up @@ -238,7 +239,7 @@ def predict_probabilities(self, stimuli):
return self.readout_classifier.predict_proba(features)

def generate_predictions(self, stimuli):
"""Call generation_fn per-stimulus, return one-hot BehavioralAssembly."""
"""Call the registered answer function per stimulus, return one-hot BehavioralAssembly."""
import numpy as np
from brainscore_core.supported_data_standards.brainio.assemblies import (
BehavioralAssembly,
Expand Down Expand Up @@ -267,7 +268,7 @@ def generate_predictions(self, stimuli):
if predicted not in label_to_idx:
import warnings
warnings.warn(
f"generation_fn returned {predicted!r}, not in "
f"answer function returned {predicted!r}, not in "
f"label_set={label_set}. Defaulting to {label_set[0]!r}."
)
predicted = label_set[0]
Expand Down
85 changes: 52 additions & 33 deletions brainscore_core/brainscore_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@
from .selection import CompositeSelector, LayerSelector, UnitSelector, _promote_to_selector
from .streaming import StreamEvent
from .streaming_helpers import (
_drain_stream_events as _drain_neural_stream_events,
drain_session_inputs as _drain_neural_stream_events,
_reconstruct_stimulus_set_from_events as _reconstruct_neural_stimulus_set,
_requested_output_channels,
_stimuli_from_stream_session as _neural_stimuli_from_stream_session,
session_stimuli as _neural_stimuli_from_stream_session,
)


Expand All @@ -48,14 +48,14 @@ def _warn_old_names(**given) -> None:
continue
warnings.warn(
f"BrainScoreModel({name}=...) is deprecated; use "
f"{_PREFERRED_NAMES[name]}. It keeps working until the public release, "
"when the old names are removed together with the interact-to-participate rename.",
f"{_PREFERRED_NAMES[name]}. It keeps working until the old names are "
"removed in a separate, announced release.",
DeprecationWarning, stacklevel=3)


class BrainScoreModel(UnifiedModel):
"""The one concrete subject, assembled from parts (extractors, region map,
answer, policy, intervention, capabilities). Evaluated through ``interact``;
answer, policy, intervention, capabilities). Evaluated through ``participate``;
``process`` is a typed convenience and the legacy benchmark methods forward
to :class:`~brainscore_core.legacy_facade.LegacyBenchmarkFacade`."""

Expand Down Expand Up @@ -108,36 +108,38 @@ def __init__(
"""A model (or, in principle, a human) evaluated through the unified interface.

Only ``identifier`` is always required; a feature-extraction model also
needs ``region_layer_map`` and an ``activations_model`` mapping.
Capability functions are optional; each enables another evaluation path.
needs ``region_layer_map`` and an ``extractor`` (a modality -> extractor
mapping). ``answer``, ``policy`` and ``intervention`` are optional; each
enables another evaluation path.

:param identifier: unique model name (also the activation-cache key).
:param model: the object perturbation hooks / generation run against (e.g.
the torch module); may be ``None`` for a feature-extraction model whose
wrapper holds its own module.
:param region_layer_map: brain region -> layer path (or a ``UnitSelector``).
:param preprocessors: input preparation callables, declared by modality.
Configure each wrapper with its preparation function or tokenizer.
:param activations_model: extraction callable ``(stimuli, layers=...)``,
shared across declared modalities, or a modality -> extractor mapping.
Image, text, audio and video wrappers use this same slot. A mapping
declares its modalities without needing placeholder preprocessors.
:param preprocessors: compatibility parameter (deprecated). Prepare inputs
inside each extractor instead.
:param activations_model: compatibility parameter (deprecated); use
``extractor``.
:param visual_degrees: vision field-of-view; ignored by non-vision models.
:param behavioral_readout_layer: layer on which to fit a behavioral readout.
:param extractor: extraction callable or modality-to-extractor mapping.
Prepare inputs inside each extractor. Preferred over activations_model.
:param answer: generate and parse a behavioral answer.
:param policy: choose an action from an EnvironmentStep.
:param intervention: apply a StateChange and return its cleanup function.
The return value is (PerturbationApplied, cleanup).
:param generation_fn: generate-and-parse callable for instruction-following
behavioral evaluation.
:param action_fn: ``EnvironmentStep -> EnvironmentResponse`` policy for
closed-loop / embodied evaluation.
:param state_change_fn: ``StateChange -> (PerturbationApplied, cleanup)`` for
lesion / stimulation evaluation.
:param supported_modalities: inputs accepted without an extraction wrapper,
for example by a closed-weight generation function. Extractor mappings
:param extractor: extraction callable ``(stimuli, layers=...)`` or a
modality -> extractor mapping. Image, text, audio and video wrappers
all use this slot; a mapping declares its input modalities. Prepare
inputs inside each extractor.
:param answer: ``(stimulus_row, instruction, label_set) -> label``;
generates and parses a behavioral answer for instruction-following
tasks.
:param policy: ``EnvironmentStep -> EnvironmentResponse``; chooses an
action for closed-loop / embodied evaluation.
:param intervention: ``StateChange -> (PerturbationApplied, cleanup)``;
applies a lesion / stimulation and returns its cleanup function.
:param generation_fn: compatibility parameter (deprecated); use ``answer``.
:param action_fn: compatibility parameter (deprecated); use ``policy``.
:param state_change_fn: compatibility parameter (deprecated); use
``intervention``.
:param supported_modalities: inputs accepted without an extractor, for
example by a closed-weight ``answer`` function. Extractor mappings
already declare their inputs; do not repeat them here.
:param required_modalities: input modalities the model cannot run without.
:param backbone_id: shared cache key so registrations with identical weights
Expand Down Expand Up @@ -248,6 +250,16 @@ def capability_config(self):
"""Read-only registration configuration for external capability authors."""
return MappingProxyType(self._capability_config)

@property
def extractor(self) -> Mapping[str, Callable]:
"""Read-only modality -> extractor mapping, as normalized at construction.

Named like the ``extractor`` constructor argument; covers the older
``activations_model``/``preprocessors`` forms too. For observation
tools that hook the wired modules; dispatch does not read it.
"""
return MappingProxyType(self._extractors)

def capability_state(self, identifier):
"""Per-model capability state, initialized by the registry before dispatch."""
return self._capability_state[identifier]
Expand Down Expand Up @@ -472,12 +484,15 @@ def process(self, input_event: InputEvent,
multi_modality: bool = False) -> OutputEvent:
"""Evaluate one input event and return the corresponding output event.

A typed convenience, not part of the subject contract (``interact`` is
A typed convenience, not part of the subject contract (``participate`` is
the evaluation method). Dispatch is by ``input_event`` type:
a ``StimulusSet`` / ``MultimodalStimulusSet`` -> ``NeuroidAssembly`` of
recorded activations (call ``start_recording`` first); a ``StateChange``
-> ``PerturbationApplied`` (apply) or ``None`` (reset); an
``EnvironmentStep`` -> ``EnvironmentResponse`` (needs ``action_fn``).
``EnvironmentStep`` -> ``EnvironmentResponse`` (needs ``policy``).
Native benchmarks evaluate subjects through sessions or the typed
helpers in ``brainscore_core.streaming_helpers``; ``process`` is not
available on every subject.

:param input_event: the stimulus / state-change / environment-step to run.
:param multi_modality: if True, extract from every detected-and-supported
Expand All @@ -494,7 +509,7 @@ def process(self, input_event: InputEvent,
output = repeat_over_time_bins(output, time_bins)
return output

def interact(self, session) -> None:
def participate(self, session) -> None:
"""Run a session through the one registered capability that claims it.

The handler sees only the streams this model declares in
Expand All @@ -505,7 +520,7 @@ def interact(self, session) -> None:
from .capabilities.registry import _setup_once
from .session_view import DeclaredInputSession
_setup_once(self, handler)
handler.interact(self, DeclaredInputSession(session, self.in_channels))
handler.participate(self, DeclaredInputSession(session, self.in_channels))

def _handle_environment_step(self, input_event: 'EnvironmentStep') -> OutputEvent:
return self._dispatcher.handle_environment_step(input_event)
Expand All @@ -517,9 +532,13 @@ def _handle_stimulus_set(self, stimuli,
multi_modality: bool = False) -> OutputEvent:
return self._dispatcher.handle_stimulus_set(stimuli, multi_modality)

def _detect_modalities(self, stimuli) -> Set[str]:
def detect_modalities(self, stimuli) -> Set[str]:
"""Declared input modalities whose columns appear in ``stimuli``."""
return self._dispatcher.detect_modalities(stimuli)

# Compatibility alias; internal callers still use it.
_detect_modalities = detect_modalities

def _pick_modality(self, detected: Set[str]) -> str:
return self._dispatcher.pick_modality(detected)

Expand Down Expand Up @@ -658,7 +677,7 @@ def _generate_predictions(self, stimuli):
# --- Legacy benchmark surface ------------------------------------------
# Permanent: legacy benchmarks are never removed. Behavior lives in
# LegacyBenchmarkFacade, which runs each legacy call as a session through
# ``interact``; these forwards seed a facade with this model's current
# ``participate``; these forwards seed a facade with this model's current
# recording and task state so direct calls keep working.

@property
Expand Down
Loading
Loading