Skip to content
Draft
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
8 changes: 4 additions & 4 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,18 @@
// ],

"mounts": [
"source=/data/datasets,target=/home/vscode/datasets,type=bind,consistency=cached"
"source=/data/datasets,target=/home/vscode/datasets,type=bind,consistency=cached",
"source=compressionkit-huggingface,target=/home/vscode/.cache/huggingface,type=volume"
],

"forwardPorts": [6006],

"postCreateCommand": "./.devcontainer/install.sh",
"postCreateCommand": "sudo install -d -o vscode -g vscode -m 700 /home/vscode/.cache /home/vscode/.cache/huggingface && ./.devcontainer/install.sh",

"remoteEnv": {
"LD_LIBRARY_PATH": "${containerEnv:LD_LIBRARY_PATH}:/usr/local/cuda/lib64",
"PATH": "${containerEnv:PATH}:/usr/local/cuda/bin",
"TF_FORCE_GPU_ALLOW_GROWTH": "true",
"HF_TOKEN": "${localEnv:HF_TOKEN}"
"TF_FORCE_GPU_ALLOW_GROWTH": "true"
},

"customizations": {
Expand Down
21 changes: 18 additions & 3 deletions .devcontainer/install.sh
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
#!/bin/bash

set -euo pipefail

export DEBIAN_FRONTEND=noninteractive

sudo apt update
sudo apt install -y \
apt_retry() {
local attempts=0
until sudo apt-get "$@"; do
attempts=$((attempts + 1))
if (( attempts >= 24 )); then
echo "apt-get failed after ${attempts} attempts: $*" >&2
return 1
fi
echo "apt-get is unavailable; retrying in 5 seconds (${attempts}/24)..." >&2
sleep 5
done
}

apt_retry update
apt_retry install -y \
build-essential \
cmake \
make \
Expand All @@ -15,7 +30,7 @@ sudo apt install -y \
git-lfs

# Optional: reduce image size a bit
sudo apt clean
sudo apt-get clean
sudo rm -rf /var/lib/apt/lists/*

git lfs install
Expand Down
16 changes: 15 additions & 1 deletion compressionkit/configs/ecg_rvq.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import annotations

from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, model_validator

from compressionkit.configs.paths import default_datasets_dir

Expand Down Expand Up @@ -483,10 +483,24 @@ class EvaluationConfig(BaseModel):
Capped at ``num_samples``. Plots are slow to render and large to ship,
so this is typically much smaller than ``num_samples``."""
tflite_rep_batches: int = 8
"""Legacy representative-batch setting; retained for config compatibility."""
int8_calibration_frames: int = Field(default=4096, gt=0)
"""Real normalized validation frames used to calibrate INT8 LiteRT ranges."""
int8_validation_frames: int = Field(default=2048, gt=0)
"""Disjoint real validation frames used to measure INT8 encoder parity."""
int8_sampling_pool_frames: int = Field(default=65_536, gt=0)
"""Maximum validation frames reservoir-sampled for quantization partitions."""
input_bit_depth: int = 16
band_metrics: BandMetricsConfig = Field(default_factory=BandMetricsConfig)
stitching: StitchingEvalConfig = Field(default_factory=StitchingEvalConfig)

@model_validator(mode="after")
def validate_quantization_partition(self) -> EvaluationConfig:
"""Require the sampling pool to cover both disjoint partitions."""
if self.int8_sampling_pool_frames < self.int8_calibration_frames + self.int8_validation_frames:
raise ValueError("int8_sampling_pool_frames must cover calibration plus validation frames")
return self


class WandbConfig(BaseModel):
"""Weights & Biases logging configuration."""
Expand Down
16 changes: 15 additions & 1 deletion compressionkit/configs/ppg_rvq.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import annotations

from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, model_validator

from compressionkit.configs.artifact_suite import ArtifactSuiteConfig
from compressionkit.configs.paths import default_datasets_dir
Expand Down Expand Up @@ -311,11 +311,25 @@ class EvaluationConfig(BaseModel):
num_plot_samples: int = 50
"""Subset of ``num_samples`` that also receive a PNG plot artifact."""
tflite_rep_batches: int = 8
"""Legacy representative-batch setting; retained for config compatibility."""
int8_calibration_frames: int = Field(default=4096, gt=0)
"""Real normalized validation frames used to calibrate INT8 LiteRT ranges."""
int8_validation_frames: int = Field(default=2048, gt=0)
"""Disjoint real validation frames used to measure INT8 encoder parity."""
int8_sampling_pool_frames: int = Field(default=65_536, gt=0)
"""Maximum validation frames reservoir-sampled for quantization partitions."""
input_bit_depth: int = 16
band_metrics: BandMetricsConfig = Field(default_factory=BandMetricsConfig)
physiokit_metrics: PhysiokitMetricsConfig = Field(default_factory=PhysiokitMetricsConfig)
long_recording: LongRecordingEvalConfig = Field(default_factory=LongRecordingEvalConfig)

@model_validator(mode="after")
def validate_quantization_partition(self) -> EvaluationConfig:
"""Require the sampling pool to cover both disjoint partitions."""
if self.int8_sampling_pool_frames < self.int8_calibration_frames + self.int8_validation_frames:
raise ValueError("int8_sampling_pool_frames must cover calibration plus validation frames")
return self


class WandbConfig(BaseModel):
"""Weights & Biases logging configuration."""
Expand Down
124 changes: 113 additions & 11 deletions compressionkit/experiments/repackage.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,88 @@ def finalize_release_metadata(output_dir: Path, scorecard_payload: dict[str, obj
write_checksums(output_dir)


def collect_release_quantization_frames(
experiment: GoldenExperiment,
run_dir: Path,
) -> tuple[np.ndarray, np.ndarray, dict[str, object]]:
"""Rebuild disjoint real, training-preprocessed INT8 frame partitions.

The release repackage path must not calibrate a model trained on normalized
physiological windows with raw synthetic waveforms. This helper reloads the
golden's saved Pydantic config and delegates to its normal dataset builder,
so resampling, framing, and normalization remain identical to training.
"""
from compressionkit.trainers.common import collect_disjoint_quantization_datasets

config_path = run_dir / "config.json"
if not config_path.is_file():
raise FileNotFoundError(f"Missing saved golden config: {config_path}")
config_payload = config_path.read_text()

if experiment.modality == "ppg":
from compressionkit.configs.ppg_rvq import PpgRvqConfig
from compressionkit.preprocessing.ppg import build_augmenter, build_preprocessor
from compressionkit.trainers.ppg_rvq import build_datasets

cfg = PpgRvqConfig.model_validate_json(config_payload)
preprocessor = build_preprocessor(
frame_size=cfg.data.frame_size,
epsilon=cfg.data.epsilon,
seed=cfg.data.shuffle_seed,
)
augmenter = build_augmenter(tuple(cfg.data.gaussian_noise), aug_cfg=cfg.data.augmentation)
_, val_ds, _, _ = build_datasets(cfg, preprocessor, augmenter)
sample_rate = cfg.data.sampling_rate
elif experiment.modality == "ecg":
from compressionkit.configs.ecg_rvq import EcgRvqConfig
from compressionkit.preprocessing.ecg import build_augmenter, build_preprocessor
from compressionkit.trainers.ecg_rvq import build_datasets

cfg = EcgRvqConfig.model_validate_json(config_payload)
preprocessor = build_preprocessor(
frame_size=cfg.data.frame_size,
epsilon=cfg.data.epsilon,
seed=cfg.data.shuffle_seed,
)
augmenter = build_augmenter(
aug_cfg=cfg.data.augmentation,
sample_rate=cfg.data.effective_sample_rate,
seed=cfg.data.shuffle_seed,
)
_, val_ds, _, _ = build_datasets(cfg, preprocessor, augmenter)
sample_rate = cfg.data.effective_sample_rate
else:
raise ValueError(f"Unsupported RVQ modality {experiment.modality!r}")

calibration_frames, validation_frames = collect_disjoint_quantization_datasets(
val_ds,
calibration_frames=cfg.evaluation.int8_calibration_frames,
validation_frames=cfg.evaluation.int8_validation_frames,
sampling_pool_frames=cfg.evaluation.int8_sampling_pool_frames,
seed=cfg.data.shuffle_seed,
)
contract: dict[str, object] = {
"format_version": 1,
"sample_rate_hz": sample_rate,
"frame_size": cfg.data.frame_size,
"input_shape": [1, 1, cfg.data.frame_size, 1],
"normalization": {
"kind": "per_frame_layer_norm",
"mean": "mean over all samples in each frame",
"variance": "mean squared deviation over all samples in each frame",
"epsilon": cfg.data.epsilon,
"inverse_for_display": "raw = normalized * sqrt(variance + epsilon) + mean",
},
"int8_quantization": {
"calibration_frames": int(calibration_frames.shape[0]),
"validation_frames": int(validation_frames.shape[0]),
"sampling_pool_frames": cfg.evaluation.int8_sampling_pool_frames,
"sampling_method": "seeded reservoir sample; disjoint calibration and validation partitions",
},
}
return calibration_frames, validation_frames, contract


def repackage_rvq_golden(
experiment: GoldenExperiment,
*,
Expand All @@ -57,7 +139,7 @@ def repackage_rvq_golden(
import keras

from compressionkit.export.deploy import export_for_deployment
from compressionkit.export.stimulus import export_stimulus_npz, generate_stimulus
from compressionkit.export.stimulus import export_stimulus_npz

output_dir = output_dir or (run_dir / "deploy")
resolved_scorecard_path = resolve_scorecard_path(run_dir, scorecard_path)
Expand All @@ -82,17 +164,13 @@ def repackage_rvq_golden(
else:
frame_size = input_shape[-1]

rep_dataset = generate_stimulus(
modality=experiment.modality,
num_samples=100,
frame_size=frame_size,
sample_rate=experiment.sample_rate,
seed=42,
rep_dataset, quantization_validation_dataset, preprocessing_contract = collect_release_quantization_frames(
experiment, run_dir
)
if len(input_shape) == 4:
rep_dataset = rep_dataset.reshape(-1, 1, frame_size, 1)
elif len(input_shape) == 3:
rep_dataset = rep_dataset[..., np.newaxis]
if tuple(rep_dataset.shape[1:]) != tuple(input_shape[1:]):
raise ValueError(
f"Calibration frame shape {rep_dataset.shape[1:]} does not match encoder input {input_shape[1:]}"
)

model_card_info: dict[str, object] = {
"experiment_id": experiment.experiment_id,
Expand All @@ -101,7 +179,22 @@ def repackage_rvq_golden(
"sample_rate": experiment.sample_rate,
"compression_ratio": experiment.compression_ratio,
"license": "other",
"preprocessing_contract": preprocessing_contract,
}
precision_paths = [
run_dir.parent / "precision-study" / f"{experiment.experiment_id}.json",
run_dir.parent / "rvq_encoder_precision_comparison.json",
]
for precision_path in precision_paths:
if not precision_path.is_file():
continue
precision_payload = json.loads(precision_path.read_text())
variants = precision_payload.get("experiments", {}).get(experiment.experiment_id, {}).get("variants", {})
if isinstance(variants, dict):
model_card_info["encoder_precision_report"] = {
name: payload.get("report", {}) for name, payload in variants.items() if isinstance(payload, dict)
}
break
if scorecard_payload is not None:
model_card_info["scorecard_summary"] = scorecard_payload

Expand All @@ -116,6 +209,7 @@ def repackage_rvq_golden(
decoder=decoder,
rvq_weights=rvq_weights,
rep_dataset=rep_dataset,
quantization_validation_dataset=quantization_validation_dataset,
output_dir=output_dir,
sample_inputs=sample_inputs,
sample_targets=sample_inputs,
Expand All @@ -125,6 +219,8 @@ def repackage_rvq_golden(
export_decoder_int8=export_decoder_int8,
model_card_info=model_card_info,
)
quantization_report = json.loads((output_dir / "quantization_report.json").read_text())
report_metrics = quantization_report["metrics"]
export_stimulus_npz(
modality=experiment.modality,
output_path=output_dir / "sample_stimulus.npz",
Expand All @@ -140,10 +236,16 @@ def repackage_rvq_golden(
"deploy_dir": str(output_dir),
"scorecard_path": str(resolved_scorecard_path) if resolved_scorecard_path is not None else None,
"artifacts": artifacts.as_dict(),
"quantization_report": {
"passed": quantization_report["passed"],
"reconstruction_prd_percent_p90": report_metrics["reconstruction_prd_percent_p90"],
"encoder_input_saturation_fraction_max": report_metrics["encoder_input_saturation_fraction_max"],
},
}


__all__ = [
"collect_release_quantization_frames",
"finalize_release_metadata",
"load_scorecard_payload",
"repackage_rvq_golden",
Expand Down
37 changes: 37 additions & 0 deletions compressionkit/export/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,27 @@
export_codebooks_npz,
extract_codebooks,
)
from compressionkit.export.demo_ecg import (
EcgDemoClip,
EcgDemoExport,
EcgDemoQuality,
assess_ecg_demo_clip,
export_ecg_demo_csvs,
generate_ecg_demo_clips,
)
from compressionkit.export.demo_ppg import PpgDemoClip, PpgDemoQuality, assess_ppg_demo_clip, generate_ppg_demo_clips
from compressionkit.export.demo_recordings import (
DemoRecordingsExport,
attach_demo_recordings_to_deploy,
export_demo_recordings,
)
from compressionkit.export.deploy import DeploymentArtifacts, export_for_deployment, sync_scorecard_to_deploy
from compressionkit.export.quantization import (
RvqEncoderQuantizationReport,
evaluate_rvq_encoder_quantization,
require_rvq_encoder_quantization_report,
write_rvq_encoder_quantization_report,
)
from compressionkit.export.release import (
build_model_card,
build_release_metadata,
Expand All @@ -24,25 +44,42 @@
from compressionkit.export.validate import DeployValidationResult, validate_deploy_package

__all__ = [
"DemoRecordingsExport",
"DeployValidationResult",
"DeploymentArtifacts",
"EcgDemoClip",
"EcgDemoExport",
"EcgDemoQuality",
"PpgDemoClip",
"PpgDemoQuality",
"RvqEncoderQuantizationReport",
"SpihtDeploymentArtifacts",
"assess_ecg_demo_clip",
"assess_ppg_demo_clip",
"attach_demo_recordings_to_deploy",
"build_model_card",
"build_release_metadata",
"evaluate_rvq_encoder_quantization",
"export_codebooks_header",
"export_codebooks_npz",
"export_decoder_tflite",
"export_demo_recordings",
"export_ecg_demo_csvs",
"export_encoder_tflite",
"export_for_deployment",
"export_spiht_deploy",
"export_stimulus_npz",
"extract_codebooks",
"generate_ecg_demo_clips",
"generate_ppg_demo_clips",
"generate_stimulus",
"require_rvq_encoder_quantization_report",
"sha256_file",
"sync_scorecard_to_deploy",
"validate_deploy_package",
"write_checksums",
"write_json",
"write_model_card",
"write_rvq_encoder_quantization_report",
"write_scorecard_artifact",
]
Loading
Loading