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
1 change: 1 addition & 0 deletions .config/clang-tidy/measured-sources.txt
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,7 @@ core/test/test_picture_v2.c
core/test/test_pixel_format_edge_coverage.c
core/test/test_pool_percentile.c
core/test/test_predict.c
core/test/test_predict_rust_ops.c
core/test/test_prescaled_plane_int_index.c
core/test/test_propagate_metadata.c
core/test/test_psnr.c
Expand Down
10 changes: 10 additions & 0 deletions .config/lint-exceptions.d/clang-tidy-coverage.toml
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,11 @@ path = "core/src/feature/tad_rust.c"
reason = "Built only with -Denable_rust_features=true, which needs cargo and cbindgen; the dev image has neither, so no lane configures it. Needs a Rust-enabled lane."
expires = 2026-12-31

[[exception]]
path = "core/src/rust/shim/rust_predict.c"
reason = "Built only with -Denable_rust_features=true, which needs cargo; the dev image has no Rust toolchain yet (ADR-1713 follow-up: a pinned toolchain in dev/Containerfile), so no lane configures it. Needs a Rust-enabled lane."
expires = 2026-12-31

[[exception]]
path = "core/src/rust/shim/rust_twins.cpp"
reason = "Built only with -Denable_rust_features=true, which needs cargo; the dev image has no Rust toolchain yet (ADR-1713 follow-up: a pinned toolchain in dev/Containerfile), so no lane configures it. Needs a Rust-enabled lane."
Expand All @@ -173,6 +178,11 @@ path = "core/test/test_rust_abi_layout.c"
reason = "Built only with -Denable_rust_features=true, which needs cargo; the dev image has no Rust toolchain yet (ADR-1713 follow-up: a pinned toolchain in dev/Containerfile), so no lane configures it. Needs a Rust-enabled lane."
expires = 2026-12-31

[[exception]]
path = "core/test/test_rust_predict.c"
reason = "Built only with -Denable_rust_features=true, which needs cargo; the dev image has no Rust toolchain yet (ADR-1713 follow-up: a pinned toolchain in dev/Containerfile), so no lane configures it. Needs a Rust-enabled lane."
expires = 2026-12-31

[[exception]]
path = "core/test/test_rust_twin_registry.c"
reason = "Built only with -Denable_rust_features=true, which needs cargo; the dev image has no Rust toolchain yet (ADR-1713 follow-up: a pinned toolchain in dev/Containerfile), so no lane configures it. Needs a Rust-enabled lane."
Expand Down
9 changes: 8 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,10 @@ repos:
# Pelorus interop sources, headers, and shared conformance fixture are
# a vendored read-only mirror (ADR-1113). The wrapper exempts only paths
# in the shared exact-path manifest; an added pelorus_*.cpp stays linted.
exclude: '^subprojects/|^core/test/data/'
# core/src/rust/include/ holds the cbindgen output, committed byte for byte
# as cbindgen 0.29.4 writes it (ADR-1713); scripts/dev/rust-abi-header.sh
# --check compares it with a fresh run, so a formatter must not touch it.
exclude: '^subprojects/|^core/test/data/|^core/src/rust/include/'
# `.hip` and `.metal` have no identify tag, so `types_or` above never selects them;
# this second entry reads them by extension, with the same wrapper, the same
# `.clang-format` and the same pin. `scripts/ci/exact_twins.d/*.hip` are parity-gate
Expand Down Expand Up @@ -187,6 +190,10 @@ repos:
rev: v1.7.12
hooks:
- id: actionlint
# ADR-2199: actionlint deadlocks when a `run:` script is larger than the pipe
# it writes to shellcheck before starting it, which a user holding many
# pipes makes common. The wrapper runs it under a deadline and fails loudly.
entry: python3 scripts/ci/run_actionlint.py

# Composite actions under .github/actions/ are not workflows: actionlint rejects
# them ("jobs section is missing"). The GitHub action schema is checked here, the
Expand Down
8 changes: 8 additions & 0 deletions .standards.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@ exceptions:
path: "core/src/interop/pelorus_version.c"
reason: "Byte-identical mirror of VMAFx/pelorus (ADR-1113; listed in scripts/ci/pelorus-mirror-paths.txt, which tidy-ratchet.py refuses as a translation unit): tidied in pelorus CI (libpelorus build, test, format, tidy) at the pinned SHA; a fix lands upstream and is re-vendored by scripts/sync-pelorus-interop.sh."
expires: "2027-01-04"
- rule: clang-tidy-coverage
path: "core/src/rust/shim/rust_predict.c"
reason: "Built only with -Denable_rust_features=true, which needs cargo; the dev image has no Rust toolchain yet (ADR-1713 follow-up: a pinned toolchain in dev/Containerfile), so no lane configures it. Needs a Rust-enabled lane."
expires: "2026-12-31"
- rule: clang-tidy-coverage
path: "core/src/rust/shim/rust_twins.cpp"
reason: "Built only with -Denable_rust_features=true, which needs cargo; the dev image has no Rust toolchain yet (ADR-1713 follow-up: a pinned toolchain in dev/Containerfile), so no lane configures it. Needs a Rust-enabled lane."
Expand All @@ -158,6 +162,10 @@ exceptions:
path: "core/test/test_rust_cambi_kernels.c"
reason: "Built only with -Denable_rust_features=true, which needs cargo; the dev image has no Rust toolchain yet (ADR-1713 follow-up: a pinned toolchain in dev/Containerfile), so no lane configures it. Needs a Rust-enabled lane."
expires: "2026-12-31"
- rule: clang-tidy-coverage
path: "core/test/test_rust_predict.c"
reason: "Built only with -Denable_rust_features=true, which needs cargo; the dev image has no Rust toolchain yet (ADR-1713 follow-up: a pinned toolchain in dev/Containerfile), so no lane configures it. Needs a Rust-enabled lane."
expires: "2026-12-31"
- rule: clang-tidy-coverage
path: "core/test/test_rust_twin_registry.c"
reason: "Built only with -Denable_rust_features=true, which needs cargo; the dev image has no Rust toolchain yet (ADR-1713 follow-up: a pinned toolchain in dev/Containerfile), so no lane configures it. Needs a Rust-enabled lane."
Expand Down
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ lint-go:
lint-actions:
$(call require-tool,actionlint,go install github.com/rhysd/actionlint/cmd/actionlint@v1.7.12)
@echo "--- actionlint (.github/workflows) ---"
@actionlint
@python3 scripts/ci/run_actionlint.py
@echo "--- composite actions (.github/actions): structure + shellcheck ---"
@python3 scripts/ci/check_composite_actions.py

Expand Down Expand Up @@ -534,6 +534,7 @@ lint-md:
# fragments only borrow the `.hip` extension; the Pelorus mirror is filtered out.
CLANG_FORMAT_FILES = git ls-files '*.c' '*.h' '*.cpp' '*.hpp' '*.cu' '*.cuh' '*.hip' '*.metal' \
| grep -v '^subprojects/' | grep -v '^core/test/data/' | grep -v '^scripts/ci/exact_twins\.d/' \
| grep -v '^core/src/rust/include/' \
| python3 scripts/ci/pelorus_mirror.py filter

# Formatters — writes changes.
Expand Down
6 changes: 6 additions & 0 deletions changelog.d/added/rc4-adm-rust-twin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
- **Rust integer ADM extractor (`adm_rust`)**: builds configured with
`-Denable_rust_features=true` contain a Rust port of the fixed-point `adm`
extractor. It takes every option of `adm` and returns its `adm2`, `aim`,
`adm3`, scale and debug scores bit for bit; select it with
`VMAF_FEATURE_IMPL=rust` or `--feature adm_rust`
([ADM](docs/metrics/adm.md#rust-implementation-adm_rust), #1723, ADR-1713).
6 changes: 6 additions & 0 deletions changelog.d/added/rust-model-prediction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
- **The prediction step of the `vmaf_v1.0.16*` models can run in Rust.** A build
with `-Denable_rust_features=true` and `VMAF_FEATURE_IMPL=rust` evaluates the
feature normalisation, chroma correction, nu-SVR, score transform and clip in
the new `vmafx-predict` crate. The score is bit-identical to the C predictor
at `--precision max`; the C predictor stays the default. See
[Models](docs/models/overview.md#rust-prediction-experimental).
6 changes: 6 additions & 0 deletions changelog.d/added/rust-motion-twin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
- **The integer `motion` extractor has a Rust twin, `motion_rust`, that returns the C
extractor's scores bit for bit.**
With `-Denable_rust_features=true`, `VMAF_FEATURE_IMPL=rust` (or
`--feature motion_rust`) computes `motion_sad_score`, `motion2` and `motion3`,
including the five-frame window and the moving average, in Rust; the default
stays the C extractor. See [Motion](docs/metrics/motion.md#rust-implementation).
6 changes: 6 additions & 0 deletions changelog.d/changed/actionlint-bounded-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
- **The `actionlint` hook and `make lint-actions` cannot hang
([ADR-2199](docs/adr/2199-actionlint-bounded-run.md)).** They run actionlint
through `scripts/ci/run_actionlint.py`, which gives it 90 seconds
(`ACTIONLINT_TIMEOUT_S`) and fails with exit 124 and the cause named instead
of hanging a commit or a push when a `run:` script is larger than the pipe
actionlint writes it to (a user over `fs.pipe-user-pages-soft`).
3 changes: 1 addition & 2 deletions changelog.d/fixed/api-generator-format-test-pin.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,4 @@
the 23.1.2 the repository pins, so the test failed on every hosted leg. It
now uses only the pinned major (`VMAFX_CLANG_FORMAT` names one explicitly)
and skips, naming the version it found, otherwise; the Tooling Tests job
installs the pinned release. `test_gpu_picture_pool_uaf` no longer writes
its deliberately huge allocation on hosts that overcommit memory.
installs the pinned release.
56 changes: 56 additions & 0 deletions core/src/AGENTS.d/rust-model-prediction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
paths:
- core/src/predict.c
- core/src/predict.h
- core/src/predict_internal.h
- core/src/model.h
- core/src/model_lifetime.c
- core/src/rust/predict/**
- core/src/rust/shim/rust_predict.c
- core/src/rust/shim/rust_predict.h
invariant: Rust predictor returns C predictor's score bit for bit; change to one changes other in same PR.
---
<!-- markdownlint-disable MD013 -->
# Rust model prediction (RC4 lane P, #1723)

`core/src/rust/predict/` (`vmafx-predict`) is `predict.c`'s post-gather
arithmetic, statement for statement: `normalize()`, `post_process_feature_from_another()`,
`svm_predict()` (EPSILON_SVR / NU_SVR with linear, polynomial and RBF
kernels), `denormalize()`, `transform()`, `piecewise_*()` and `clip()`.
`VMAF_FEATURE_IMPL=rust` selects it per process; C predictor stays default.

1. **Change to any of those C routines changes `core/src/rust/predict/src/` in
same PR.** `core/test/test_rust_predict.c` (suite `rust`) compares both
predictors bit for bit on every `vmaf_v1.0.16*` model and on edited copies;
`scripts/ci/rust_twin_diff.py --models` does same end to end.
2. **`vmaf_predict_score_at_index()` keeps score gather in C**
(`predict_load_feature_score()`: collector lock, `-EAGAIN`, name cache) and
append of prediction; only `predict_compute_c()` / `predict_compute_rust()`
differ. Sync keeps C body in `predict_compute_c()`.
3. **No silent fallback.** Model Rust predictor refuses (classification SVM,
precomputed or sigmoid kernel, unknown normalisation) runs on C predictor
and WARNING says so; table's `create` returns `-ENOTSUP` for it. With
`VMAF_FEATURE_IMPL=rust` and no table installed (build without Rust) C
predictor runs and WARNING says that too. Mode comes from
`vmaf_feature_impl_rust_requested()`, framework's one reader of
variable; value other than `c` or `rust` fails prediction with
`-EINVAL`.
4. **Libm**: `exp` goes through `vmafx_fex::libm` (C's `exp`); never `f64::exp`,
never `mul_add`; `-gamma * sum` is `(-gamma) * sum`.
5. Scratch of Rust call is C predictor's own `predict_nodes` (`struct
svm_node` layout asserted in `rust_predict.c`); Rust predictor does not
allocate per frame.
6. **Only libvmaf links Rust archive (ADR-1713).** `predict.c`, `model.c` and
`model_lifetime.c` (`predict_c` archive and test binaries compiling them)
reach Rust predictor only through `struct VmafRustPredictOps` table
declared in `predict.h` and stored in `predict.c`
(`vmaf_predict_install_rust_ops()`; NULL = C predictor).
`core/src/rust/shim/rust_predict.c` defines table on `vmafx_rs_model_*` and
`vmaf_rust_predict_install()`; it is listed in `rust_shim_sources`, and
`vmaf_ctx_subsystems_init()` calls installer next to
`vmaf_rust_twins_install()` under `#if HAVE_RUST_FEATURES`. No Rust symbol,
no Rust include and no `HAVE_RUST_FEATURES` branch belongs in those three
files. `vmaf_model_destroy()` frees handle through
`vmaf_rust_predict_destroy()`. `core/test/test_predict_rust_ops.c` (every
build, stand-in table) guards routing, `test_rust_predict` installation by
`vmaf_init()`.
1 change: 1 addition & 0 deletions core/src/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Parent scope: [`../AGENTS.md`](../AGENTS.md) (core) and
| `libvmaf.c`, `percentile.h`, `bootstrap_names.h` | [pooling-and-bootstrap](AGENTS.d/pooling-and-bootstrap.md) | Pooling accumulators remain O(1) float-exact; bootstrap score names stay centralized. |
| `read_json_model.c`, `read_json_model.h` | [read-json-model](AGENTS.d/read-json-model.md) | Model feature arrays sync capacity before access and free previous names before strdup. |
| `rust/**`, `feature/feature_extractor.cpp`, `feature/feature_extractor.h`, `feature/tad_rust.c`, `libvmaf.c`, `meson.build` | [rust-extractor-framework](AGENTS.d/rust-extractor-framework.md) | One Rust archive in libvmaf only; registry reaches Rust via the shim accessor; twins inherit C descriptor. |
| `predict.c`, `predict.h`, `predict_internal.h`, `model.h`, `model_lifetime.c`, `rust/predict/**`, `rust/shim/rust_predict.c`, `rust/shim/rust_predict.h` | [rust-model-prediction](AGENTS.d/rust-model-prediction.md) | Rust predictor returns C predictor's score bit for bit; change to one changes other in same PR. |
| `predict.c`, `interop/pelorus_interop.c` | [scoring-and-pelorus-interop](AGENTS.d/scoring-and-pelorus-interop.md) | Piecewise linear mapping rejects non-finite scores; Pelorus interop validates framing and QP bounds. |
| `svm.cpp`, `svm.h` | [vendored-libsvm](AGENTS.d/vendored-libsvm.md) | Vendored libsvm preserves thread-locale isolation, JSON in-memory parser, malloc checks, and RAII Solver. |
| `vmafx/*`, `libvmaf.c`, `log.cpp`, `log.h`, `/core/test/test_vmafx_*`, `/core/test/vmafx_test_util.h`, `/core/test/vmafx_import_test_util.h`, `/core/test/vmafx_fixture_util.h`, `compat/gcc/stdatomic.h`, `vmafx.map`, `vmafx.def`, `vmafx_symbols.txt`, `/core/api/vmafx.toml`, `/core/include/vmafx/*`, `/core/test/check_exported_symbols.py`, `/scripts/codegen/**`, `/bindings/python/vmafx/*` | [vmafx-api](AGENTS.d/vmafx-api.md) | Generated from vmafx.toml, never hand-edit; engine code calls vmaf_engine_ only; frame ref = picture VmafRef. |
Expand Down
44 changes: 44 additions & 0 deletions core/src/feature/AGENTS.d/adm-rust-twin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
paths:
- core/src/feature/integer_adm.c
- core/src/feature/integer_adm.h
- core/src/feature/integer_adm_kernels.h
- core/src/feature/adm_csf_fixed_point.h
- core/src/feature/adm_cm_accumulator.h
- core/src/feature/adm_angle_flag.h
- core/src/feature/adm_score.h
- core/src/feature/barten_csf_tools.h
- core/src/rust/feature/adm/**
invariant: Integer ADM arithmetic change in C = same change in core/src/rust/feature/adm; rust_twin_diff.py adm shows 0 diffs.
---
<!-- markdownlint-disable MD013 MD032 MD060 -->
# Integer ADM Rust twin (`adm_rust`, ADR-1713)

- **Two implementations of one extractor.** `core/src/rust/feature/adm/` ports
scalar path of `integer_adm.c` and headers above statement by statement:
same integer widths and narrowing points, same float evaluation order, same
libm calls (`vmafx_fex::libm`). `adm_rust` must equal `adm` bit for bit on
every option. Change to arithmetic, option table, emitted names or their
order on C side changes crate in same PR; upstream sync touching these files
re-runs harness.
- **Map C -> Rust.** DWT and index tables: `dwt.rs`; decouple and angle flag:
`decouple.rs`; CSF stage: `csf.rs`; denominators: `den.rs`; contrast
masking: `cm.rs`; CSF weights (Watson, Barten, blended tables, fixed-point
normalisation): `csf_weights.rs`; literal tables: `tables.rs`; borders and
shift budgets: `region.rs`; finalisation and emission: `score.rs`; driver:
`lib.rs`.
- **Quirks kept on both sides.** `add_bef_shift_flt = (int32_t)(1u << 31)`
(Netflix#955, ADR-0155), scale-0 diagonal rounding term `65535`, int32
centre tap (ADR-1402), int64 clamped scale-0 excess, unclipped integer AIM
(ADR-1417), `uint32_t` exponent of scale 1..3 denominator, `int64_t / float`
division of scale 1..3 numerator.
- **Float tables.** C tables are double literals narrowed to `float`; crate
writes each as `f(<double literal>)`. `tables.rs` holds every entry against
C bit pattern (`tables_c.rs`, dumped from both headers).
- **Constants C compiler folds** (`cos(M_PI/180)^2`, Barten luminance anchors,
`barten_rod_cone_sens(100)`): crate computes them with glibc at init; equal
to GCC's folded values on glibc (unit tests pin bits). Other libm that
differs: write C value as literal, never guess.
- **Evidence.** `scripts/ci/rust_twin_diff.py --feature adm` over `netflix`,
`checker1`, `checker10`, `sparks10`, `bbb4k` for default and four
`vmaf_v1.0.16*` option sets; `cargo test -p vmafx-fex-adm`.
1 change: 1 addition & 0 deletions core/src/feature/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ PSNR, SSIM, MS-SSIM, LPIPS, …). Parent: [../../AGENTS.md](../../AGENTS.md).
| `integer_adm.c`, `adm_tools.c` | [adm-kernels-restructure](AGENTS.d/adm-kernels-restructure.md) | Integer ADM kernels header separation, AIM clipping differences, and adm_min_val clamping. |
| `integer_adm_kernels.h`, `sycl/integer_adm_sycl.cpp`, `metal/integer_adm_metal.mm`, `metal/integer_adm_metal_host.c`, `/core/test/test_integer_adm_quant_step.c`, `/core/test/test_integer_adm_quant_step_contract.py` | [adm-quant-step](AGENTS.d/adm-quant-step.md) | dwt_quant_step() exponent = float product of k, temp, temp, as upstream; CPU and SYCL copies change together. |
| `integer_adm.c`, `integer_adm_kernels.h`, `arm64/adm_neon.c`, `adm_cm_accumulator.h`, `x86/adm_avx2.c`, `x86/adm_avx512.c` | [adm-rounding](AGENTS.d/adm-rounding.md) | Integer ADM i4_adm_cm rounding overflow, row rounding, scale-0 masking, and gain limits. |
| `integer_adm.c`, `integer_adm.h`, `integer_adm_kernels.h`, `adm_csf_fixed_point.h`, `adm_cm_accumulator.h`, `adm_angle_flag.h`, `adm_score.h`, `barten_csf_tools.h`, `/core/src/rust/feature/adm/**` | [adm-rust-twin](AGENTS.d/adm-rust-twin.md) | Integer ADM arithmetic change in C = same change in core/src/rust/feature/adm; rust_twin_diff.py adm shows 0 diffs. |
| `feature_extractor.h` | [allocation-limits](AGENTS.d/allocation-limits.md) | Per-frame malloc or aligned_malloc for geometry-sized buffers is strictly prohibited. |
| `feature_extractor.cpp`, `feature_collector.cpp` | [ansnr-removal](AGENTS.d/ansnr-removal.md) | ANSNR and float_ansnr extractors remain removed per ADR-0865. |
| `brisque.c`, `brisque_math.h` | [brisque](AGENTS.d/brisque.md) | BRISQUE MATLAB pipeline parity and numerical stability assertions. |
Expand Down
2 changes: 2 additions & 0 deletions core/src/libvmaf.c
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ __attribute__((weak)) char __libc_single_threaded = 1;
#include "vcs_version.h"
#include "vmafx/engine.h"
#if HAVE_RUST_FEATURES
#include "rust/shim/rust_predict.h"
#include "rust/shim/rust_twins.h"
#endif

Expand Down Expand Up @@ -361,6 +362,7 @@ static int vmaf_ctx_subsystems_init(VmafContext *v)
* registry first, so the audit covers them too. */
#if HAVE_RUST_FEATURES
vmaf_rust_twins_install();
vmaf_rust_predict_install();
#endif
int err = vmaf_feature_extractor_list_audit();
if (err)
Expand Down
1 change: 1 addition & 0 deletions core/src/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -2352,6 +2352,7 @@ if is_rust_enabled
rust_shim_sources = [
src_dir + 'rust/shim/rust_twins.cpp',
feature_src_dir + 'tad_rust.c',
src_dir + 'rust/shim/rust_predict.c',
]
endif

Expand Down
9 changes: 9 additions & 0 deletions core/src/model.h
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,15 @@ struct VmafModel {
* one owner, the caller). vmaf_model_destroy() drops one owner and frees the
* model with the last. */
struct VmafRef *owners;
/* Rust predictor (RC4 lane P, ADR-1713): state 0 = not decided, 1 =
* `rust_predict` ready, 2 = the C predictor runs (VMAF_FEATURE_IMPL is not
* `rust`, or a WARNING said why). `rust_predict` is the handle the installed
* `VmafRustPredictOps` table (predict.h) created; `predict_raw` is the
* `n_features` raw-score scratch of the Rust path. All three are set under
* predict_cache_lock; the pointers stay NULL unless VMAF_FEATURE_IMPL=rust. */
int rust_predict_state;
void *rust_predict;
double *predict_raw;
};

struct VmafModelCollection {
Expand Down
3 changes: 3 additions & 0 deletions core/src/model_lifetime.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

#include "dict.h"
#include "model.h"
#include "predict.h"
#include "ref.h"
#include "svm.h"

Expand Down Expand Up @@ -72,6 +73,8 @@ void vmaf_model_destroy(VmafModel *model)
free(model->feature);
free(model->score_transform.knots.list);
free(model->predict_nodes);
vmaf_rust_predict_destroy(model);
free(model->predict_raw);
if (model->predict_feature_names) {
for (unsigned i = 0; i < model->n_features; i++) {
free(model->predict_feature_names[i]);
Expand Down
Loading
Loading