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
23 changes: 21 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,18 @@ if(BUILD_TESTING)
add_test(NAME clap2_contract
COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/models/clap2_invalid.py"
"${CMAKE_CURRENT_BINARY_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/clap2-smoke")
set_tests_properties(clap2_contract PROPERTIES TIMEOUT 180)
set_tests_properties(clap2_contract PROPERTIES TIMEOUT 180 FIXTURES_SETUP clap2_smoke)
add_executable(clap2-rope-test tests/models/clap2-rope-test.cpp)
target_include_directories(clap2-rope-test PRIVATE src)
target_link_libraries(clap2-rope-test PRIVATE clap ggml::ggml nlohmann_json::nlohmann_json)
add_test(NAME clap2_rope
COMMAND clap2-rope-test "${CMAKE_CURRENT_BINARY_DIR}/clap2-smoke/model-f32.gguf")
set_tests_properties(clap2_rope PROPERTIES
FIXTURES_REQUIRED clap2_smoke TIMEOUT 60 ENVIRONMENT "OMP_NUM_THREADS=2")
add_test(NAME clap2_oracle_config
COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/models/clap2_oracle_test.py")
add_test(NAME clap2_sizes
COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/models/clap2_sizes.py")
option(CLAP2_MODEL_PARITY "Run optional pinned open_clip CLAP2 numerical oracles" OFF)
set(CLAP2_REFERENCE_SOURCE "" CACHE PATH "Pinned official open_clip source directory")
set(CLAP2_REFERENCE_ASSETS "" CACHE PATH "Pinned CLAP2 config/tokenizer/checkpoint-header assets")
Expand All @@ -201,7 +212,15 @@ if(BUILD_TESTING)
COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/models/clap2_parity.py"
"${CMAKE_CURRENT_BINARY_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/clap2-fixture"
"${CLAP2_REFERENCE_SOURCE}" "${CLAP2_REFERENCE_ASSETS}")
set_tests_properties(clap2_model_parity PROPERTIES TIMEOUT 600)
set_tests_properties(clap2_model_parity PROPERTIES TIMEOUT 600 RESOURCE_LOCK clap2_oracles)
foreach(profile IN ITEMS xxs-head56 asymmetric-head64)
add_test(NAME clap2_model_parity_${profile}
COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/models/clap2_parity.py"
"${CMAKE_CURRENT_BINARY_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/clap2-fixture-${profile}"
"${CLAP2_REFERENCE_SOURCE}" "${CLAP2_REFERENCE_ASSETS}" --profile "${profile}")
set_tests_properties(clap2_model_parity_${profile} PROPERTIES
TIMEOUT 600 RESOURCE_LOCK clap2_oracles)
endforeach()
endif()
add_test(NAME clamp3_bundle_contract
COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/models/clamp3_bundle_contract.py" "$<TARGET_FILE:clap-cli>")
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ C++20/ggml implementations of audio embedding inference features:
| Model | Text | Audio | Embedding dimensions | Sample rate |
| --- | --- | --- | --- | --- |
| [CLAP](https://huggingface.co/laion/larger_clap_music) | ✅ | ✅ | 512 | 48 kHz |
| [CLAP2](https://huggingface.co/laion/clap2-large-audioxl-s1B) | ✅ | ✅ | 960 | 32 kHz |
| [CLAP2 Large](https://huggingface.co/laion/clap2-large-audioxl-s1B) | ✅ | ✅ | 960 | 32 kHz |
| [CLAP2 Medium](https://huggingface.co/laion/clap2-medium-audioxl-s1B) | ✅ | ✅ | 640 | 32 kHz |
| [CLAP2 Small](https://huggingface.co/laion/clap2-small-audioxl-s1B) | ✅ | ✅ | 512 | 32 kHz |
| [CLAP2 XS](https://huggingface.co/laion/clap2-xs-audioxl-s1B) | ✅ | ✅ | 320 | 32 kHz |
| [CLAP2 XXS](https://huggingface.co/laion/clap2-xxs-audioxl-s1B) | ✅ | ✅ | 224 | 32 kHz |
| [CLaMP3](https://github.com/sanderwood/clamp3) | ✅ | ✅ | 768 | 24 kHz |
| [MERT v1 95M](https://huggingface.co/m-a-p/MERT-v1-95M) | ❌ | ✅ | 768 | 24 kHz |
| [MERT v1 330M](https://huggingface.co/m-a-p/MERT-v1-330M) | ❌ | ✅ | 1,024 | 24 kHz |
Expand Down
33 changes: 27 additions & 6 deletions conversion/clap2.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,20 @@

from .base import ModelBase, string

SOURCE_REPO = 'laion/clap2-large-audioxl-s1B'
SOURCE_REVISION = 'e836feaa79cd1f7de939fe77065819f48fc44e4c'
# Published tower dimensions (width, FFN width, heads, depth), projection and
# immutable HF revisions. In particular XXS uses 56-dimensional attention heads.
PUBLISHED_MODELS = {
'large': ((1024, 2816, 16, 12), (960, 2560, 15, 11), 960,
'e836feaa79cd1f7de939fe77065819f48fc44e4c'),
'medium': ((640, 1728, 10, 12), (640, 1728, 10, 11), 640,
'3a8d9d79cb98e4d409fce24092a69718a821a7ad'),
'small': ((512, 1408, 8, 12), (512, 1408, 8, 11), 512,
'9891ee368dbec866dab1919f163b7e091ab0ed37'),
'xs': ((320, 896, 5, 12), (320, 896, 5, 12), 320,
'adc510748f0b759918ba4aaee94fc864e05c48d9'),
'xxs': ((224, 640, 4, 12), (224, 640, 4, 12), 224,
'adaca9be91f5045d06e10569de3075e1ef37f823'),
}
OPEN_CLIP_REVISION = 'f9a0948d573009f8fa3e530e55814cb11fb8e8bd'


Expand Down Expand Up @@ -194,8 +206,7 @@ def clap2_inputs(source, revision=None, tokenizer_path=None):
source = Path(source)
if not source.is_dir():
raise ValueError('CLAP2 source must be a checkpoint directory')
revision = SOURCE_REVISION if revision is None else revision
if not isinstance(revision, str) or re.fullmatch(r'[0-9a-fA-F]{40}', revision) is None:
if revision is not None and (not isinstance(revision, str) or re.fullmatch(r'[0-9a-fA-F]{40}', revision) is None):
raise ValueError('CLAP2 revision must be a full 40-character commit SHA')
config = _read_json(source / 'config.json')
processor = _read_json(source / 'processor_config.json')
Expand All @@ -205,12 +216,22 @@ def clap2_inputs(source, revision=None, tokenizer_path=None):
checkpoint = source / 'model.safetensors'
if not checkpoint.is_file():
raise ValueError('CLAP2 source is missing HF model.safetensors (open_clip weights are not interchangeable)')
metadata = {'general.architecture': 'clap2', 'general.name': SOURCE_REPO,
'clap2.source_repo': SOURCE_REPO, 'clap2.source_revision': revision.lower(),
metadata = {'general.architecture': 'clap2', 'general.name': 'CLAP2',
'clap2.open_clip_revision': OPEN_CLIP_REVISION,
'clap2.config': json.dumps(config, separators=(',', ':')),
'clap2.processor_config': json.dumps(processor, separators=(',', ':')),
'clap2.tokenizer': json.dumps(tokenizer, separators=(',', ':'), ensure_ascii=False)}
fields = ('hidden_size', 'intermediate_size', 'num_attention_heads', 'num_hidden_layers')
signature = tuple(tuple(config[branch + '_config'][key] for key in fields)
for branch in ('audio', 'text')) + (config['projection_dim'],)
for size, (*dimensions, pinned_revision) in PUBLISHED_MODELS.items():
if signature == tuple(dimensions):
repo = f'laion/clap2-{size}-audioxl-s1B'
metadata.update({'general.name': repo, 'clap2.source_repo': repo})
revision = pinned_revision if revision is None else revision
break
if revision is not None:
metadata['clap2.source_revision'] = revision.lower()
return checkpoint, metadata, config


Expand Down
2 changes: 1 addition & 1 deletion convert_hf_to_gguf.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def parse_args():
parser.add_argument('--clamp3-config', type=Path,
help='CLaMP3 combined config JSON (otherwise infer from sibling XLM-R config)')
parser.add_argument('--tokenizer', type=Path, help='CLAP2/CLaMP3/MuLan local tokenizer.json (default: beside checkpoint)')
parser.add_argument('--clap2-revision', help='full HF revision SHA for CLAP2 metadata (default: pinned laion checkpoint)')
parser.add_argument('--clap2-revision', help='full HF revision SHA for CLAP2 metadata (default: pinned revision for the detected model size)')
parser.add_argument('--mert2-revision', help='full pinned Hugging Face revision SHA for MERT2 metadata')
parser.add_argument('--muq-revision', help='full pinned Hugging Face revision SHA for MuQ metadata')
parser.add_argument('--mert-revision', help='full pinned Hugging Face revision SHA for MERT-v1 metadata')
Expand Down
2 changes: 1 addition & 1 deletion tests/models/clap2-model-test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ int main(int argc, char **argv) {
return clap_encode_audio(model.get(), wave.data(), wave.size(), fixture.at("sample_rate"), 0,
out, count);
}),
test.at("expected").get<std::vector<float>>(), tolerance, test.at("name"));
test.at("expected").get<std::vector<float>>(), test.value("tolerance", tolerance), test.at("name"));
float *out = nullptr;
size_t count = 0;
const auto status =
Expand Down
99 changes: 99 additions & 0 deletions tests/models/clap2-rope-test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// CLAP2 size-geometry regression. Uses only the tiny analytical GGUF's
// constructor metadata; rotary inputs have the exact published tower widths.
#include "model.h"

#include <algorithm>
#include <cmath>
#include <iostream>
#include <stdexcept>
#include <string>

namespace clap::detail {
struct FeatureTestAccess {
static ggml_tensor *rotate(const Clap2Model &model, Graph &graph, ggml_tensor *input, int heads, int time,
int prefix) {
return model.rotate(graph, input, heads, 10000.f, time, prefix);
}
};
} // namespace clap::detail

using namespace clap::detail;

static void rotary(const Clap2Model &model, const GraphRuntime &runtime, const char *name, int width, int heads,
bool audio) {
// A non-square frequency/time grid detects swapped axes and flattening.
// Cross both axes and exercise every head, including XXS's 56-wide heads.
const int time = 7, frequency = 8, prefix = audio ? 1 : 0;
const int tokens = audio ? time * frequency + prefix : 19;
const int head_dim = width / heads;
HostMatrix input(tokens, width);
for (size_t i = 0; i < input.data.size(); ++i)
input.data[i] = std::sin(float(i) * 0.17f) + 0.3f * std::cos(float(i) * 0.11f);
Graph graph(runtime);
auto *output = FeatureTestAccess::rotate(model, graph, graph.input(input), heads, audio ? time : 0, prefix);
const auto actual = graph.run(output);
if (actual.size() != input.data.size())
throw std::runtime_error("CLAP2 rotary shape mismatch");
float maximum = 0;
// Scalar expression of upstream timm build_rotary_pos_embed: dim/4
// inverse-frequency bands per axis, repeated interleaved. ModernText
// uses dim/2 bands and ordinary positions. No power-of-two assumption.
for (int token = 0; token < tokens; ++token)
for (int head = 0; head < heads; ++head)
for (int channel = 0; channel < head_dim; channel += 2) {
const int axis_dim = audio ? head_dim / 2 : head_dim;
const int within_axis = channel % axis_dim;
const int position = audio ? (channel < axis_dim ? std::max(0, token - prefix) / time
: std::max(0, token - prefix) % time)
: token;
const double inverse_frequency = std::exp(-std::log(10000.0) * within_axis / axis_dim);
const double angle = token < prefix ? 0.0 : position * inverse_frequency;
const int column = head * head_dim + channel;
const double even = input[token][column], odd = input[token][column + 1];
const double expected[] = {even * std::cos(angle) - odd * std::sin(angle),
odd * std::cos(angle) + even * std::sin(angle)};
for (int component = 0; component < 2; ++component) {
const float value = actual[size_t(token) * width + column + component];
if (!std::isfinite(value))
throw std::runtime_error("CLAP2 rotary produced non-finite output");
maximum = std::max(maximum, float(std::abs(value - expected[component])));
if (token < prefix && value != input[token][column + component])
throw std::runtime_error("CLAP2 audio register token was rotated");
}
}
std::cout << name << (audio ? " audio" : " text") << " width=" << width << " heads=" << heads
<< " head_dim=" << head_dim << " max_abs_error=" << maximum << '\n';
if (maximum > 3e-6f)
throw std::runtime_error("CLAP2 rotary differs from independent scalar oracle");
}

int main(int argc, char **argv) {
try {
if (argc != 2)
throw std::runtime_error("usage: clap2-rope-test TINY-CLAP2.gguf");
clap::RuntimeOptions options;
options.device = "cpu";
ModelLoader loader(options);
loader.load(argv[1]);
Clap2Model model(loader, 2);
GraphRuntime runtime(loader, 2, true);
// Published config.json dimensions from pinned laion/clap2-*-audioxl-s1B.
struct Size {
const char *name;
int audio_width, audio_heads, text_width, text_heads;
};
const Size sizes[] = {{"large", 1024, 16, 960, 15},
{"medium", 640, 10, 640, 10},
{"small", 512, 8, 512, 8},
{"xs", 320, 5, 320, 5},
{"xxs", 224, 4, 224, 4}};
for (const auto &size : sizes) {
rotary(model, runtime, size.name, size.audio_width, size.audio_heads, true);
rotary(model, runtime, size.name, size.text_width, size.text_heads, false);
}
return 0;
} catch (const std::exception &error) {
std::cerr << error.what() << '\n';
return 1;
}
}
55 changes: 49 additions & 6 deletions tests/models/clap2_SOURCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,23 @@ Relevant upstream files:
- `src/open_clip/transformer.py`: ModernTextTransformer and masked MAP pool
- `src/open_clip/tokenizer.py`: TikTokenTokenizer, `encode_ordinary` semantics

Published HF assets/checkpoint are `laion/clap2-large-audioxl-s1B` revision
`e836feaa79cd1f7de939fe77065819f48fc44e4c`. The fixture generator validates its
complete reduced-depth HF tensor-name set against the downloaded checkpoint
header. Production conversion keeps those HF tensor names unchanged.
Published HF assets/checkpoints are the five `laion/clap2-*-audioxl-s1B` releases:

| Size | HF revision | Audio / text width | Audio / text layers | Projection | Head width |
| --- | --- | --- | --- | --- | --- |
| large | `e836feaa79cd1f7de939fe77065819f48fc44e4c` | 1024 / 960 | 12 / 11 | 960 | 64 |
| medium | `3a8d9d79cb98e4d409fce24092a69718a821a7ad` | 640 / 640 | 12 / 11 | 640 | 64 |
| small | `9891ee368dbec866dab1919f163b7e091ab0ed37` | 512 / 512 | 12 / 11 | 512 | 64 |
| xs | `adc510748f0b759918ba4aaee94fc864e05c48d9` | 320 / 320 | 12 / 12 | 320 | 64 |
| xxs | `adaca9be91f5045d06e10569de3075e1ef37f823` | 224 / 224 | 12 / 12 | 224 | 56 |

`clap2_sizes.json` retains the pinned HF configurations and independently read
published tensor-schema digests for offline regressions.
Native inference derives tensor dimensions, layer counts and RoPE bands from
configuration; XXS has 14 axial frequency bands per axis, not a rounded 64-wide
head. `clap2-rope-test` checks every published tower geometry against an independent
scalar oracle on an asymmetric frequency/time grid. Production conversion keeps
the HF tensor names unchanged.

### Important text MLP layout difference

Expand Down Expand Up @@ -81,10 +94,21 @@ loader/API smoke coverage, NOT numerical architecture/frontend parity.
```

`--generate-only` prepares both GGUFs and fixture JSONs without native execution.
Use `--profile xxs-head56` to test the released XXS head geometry with small
nondegenerate towers, or `--profile asymmetric-head64` for unequal tower widths,
depths and MLP widths. The default `tiny` profile preserves the original reduced
fixture. These profiles run the same tokenizer, padding, waveform and crop-sensitive
long-audio cases in F32/F16. `--tokenizer-assets COMMON_ASSETS` can share identical
tokenizer/header assets across sizes.

`OFFICIAL_OPEN_CLIP` contains the reviewed checkout, `src/open_clip`, and
`PINNED_SHA` containing the exact source SHA above. `HF_ASSETS` contains pinned
`config.json`, `processor_config.json`, `tokenizer.json`, `tokenizer_config.json`,
and `model.safetensors.header.json`; no full weights are needed. `OUTPUT` must
`open_clip_config.json`, `identity.json` (`repo` and full `revision` SHA),
and `model.safetensors.header.json`; no full weights are needed. A full HF API
`hf-manifest.json` can supply `id` and `sha` instead of `identity.json`. The oracle
cross-checks the HF and open_clip tower dimensions before constructing a model.
`OUTPUT` must
be separate from downloaded assets and checkpoints. It is updated in place,
not recursively deleted. tiktoken may fetch/cache its standard r50k_base ranks
on first use; no model checkpoint is downloaded by these scripts.
Expand All @@ -109,7 +133,26 @@ Coverage:
counted as user tokens. The oracle verifies compact/padded inference equality
and independently tests interior/suffix padding and masked MAP pooling.
- Upstream invalid audio-patch padding invariance.
- F32/F16 storage-rounded parity under a 5e-5 maximum-absolute embedding tolerance.
- F32/F16 storage-rounded parity under a 5e-5 maximum-absolute embedding tolerance,
except the documented frontend-sensitive case below.

### FFT precision boundary

The synthetic `asymmetric-head64` profile's `multiple` pure-tone waveform spans
more than 130 dB, including mel bins close to the −100 dB clamp. The pinned CPU
Torch frontend uses an F32 MKL FFT; native inference uses the shared double FFT.
Roundoff in near-zero spectral bins can therefore change the log-mel floor and
produce an embedding difference around 1e-4 despite the same mathematical
frontend. This single waveform case carries an explicit **2e-4** comparison
bound and precision annotation in both storage modes. Other waveform cases,
all text/token cases, and published-checkpoint tolerances are unchanged.

To diagnose this boundary, compare native and official log-mel first, then inject
the same official mel into the native patchify/audio tower through a private
`FeatureTestAccess` probe. Cross-combine native/Torch powers and mel filters, and
compare official F32 versus double FFT before attributing a difference to model
geometry. The asymmetric tower agrees within 3e-7 with identical official mel;
a filterbank-only change does not resolve this FFT-specific difference.

Tiny random-weight parity is not published-checkpoint parity.

Expand Down
Loading
Loading