Skip to content

Commit fe37515

Browse files
author
Rita Law
committed
fix(mtmd/qwen3vl): grid selection rewrite + --image-max-tiles override
Rewrite Qwen3VL multi-tile grid selection: enumerate CxR grids that downscale the image in both dimensions (col*tile_px<=nx, row*tile_px<=ny), exclude 1x1 (equivalent to dyn_size), then pick within a ratio-error tolerance band by max tile count and min log-ratio error. Fall back to dyn_size when no grid fits. Add a global overview thumbnail at entries[0]. Add the --image-max-tiles CLI/env override (1..256), applied after load_hparams so GGUF defaults don't clobber it, and clamp preproc_min_tiles so a low override can't produce an empty candidate set on InternVL-family models. Guard the log-ratio division against zero-dimension images. Hardening: - Validate clip.vision.preproc_max_tiles read from the GGUF: clamp to [1,256] (the --image-max-tiles range) and fall back to 4 with a warning, matching the InternVL branch. A crafted/corrupt value could otherwise drive an oversized fitting.reserve() and OOM on load or first inference. - Size fitting.reserve() to max_tiles*ln(max_tiles) (the actual candidate count) instead of max_tiles*2, which undershot ~3x at the top of the range. - Fail a zero-dimension image gracefully (LOG_ERR + return false) instead of GGML_ASSERT, so one malformed upload fails its own request rather than aborting the whole server. Callers already treat false as a per-request error. - Doc/log consistency: reflow --image-max-tiles help to the sibling --image-* style, add the --image-tile-mode/--image-max-tiles rows to the cli/server READMEs, and log preproc_max_tiles as "(custom value)" when overridden.
1 parent e9ad5fc commit fe37515

11 files changed

Lines changed: 133 additions & 47 deletions

File tree

common/arg.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2270,6 +2270,17 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
22702270
else { throw std::invalid_argument("unknown --image-tile-mode: " + value + " (use batched, sequential, or disabled)"); }
22712271
}
22722272
).set_examples(mmproj_examples).set_env("LLAMA_ARG_IMAGE_TILE_MODE"));
2273+
add_opt(common_arg(
2274+
{"--image-max-tiles"}, "N",
2275+
"maximum number of tiles for multi-tile vision models (e.g. Qwen3VL), overrides the\n"
2276+
"value from the GGUF; use when the GGUF lacks the key or the model default is wrong\n"
2277+
"for your model size (Qwen3VL defaults to 4, too low for 8B+ variants)",
2278+
[](common_params & params, int value) {
2279+
if (value < 1) { throw std::invalid_argument("--image-max-tiles must be >= 1"); }
2280+
if (value > 256) { throw std::invalid_argument("--image-max-tiles must be <= 256"); }
2281+
params.image_max_tiles = value;
2282+
}
2283+
).set_examples(mmproj_examples).set_env("LLAMA_ARG_IMAGE_MAX_TILES"));
22732284
if (llama_supports_rpc()) {
22742285
add_opt(common_arg(
22752286
{"--rpc"}, "SERVERS",

common/common.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,7 @@ struct common_params {
595595
int image_min_tokens = -1;
596596
int image_max_tokens = -1;
597597
common_image_tile_mode image_tile_mode = COMMON_IMAGE_TILE_MODE_SEQUENTIAL;
598+
int image_max_tiles = -1; // override preproc_max_tiles from GGUF; -1 = use model default
598599

599600
// finetune
600601
struct lr_opt lr;

tools/cli/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,8 @@
165165
| `--image, --audio FILE` | path to an image or audio file. use with multimodal models, use comma-separated values for multiple files |
166166
| `--image-min-tokens N` | minimum number of tokens each image can take, only used by vision models with dynamic resolution (default: read from model)<br/>(env: LLAMA_ARG_IMAGE_MIN_TOKENS) |
167167
| `--image-max-tokens N` | maximum number of tokens each image can take, only used by vision models with dynamic resolution (default: read from model)<br/>(env: LLAMA_ARG_IMAGE_MAX_TOKENS) |
168+
| `--image-tile-mode MODE` | tile encoding mode for multi-tile vision models (e.g. Qwen3VL):<br/> batched - all tiles in one forward pass<br/> sequential - tiles encoded one-by-one (default)<br/> disabled - tiling disabled, single tile only<br/>(env: LLAMA_ARG_IMAGE_TILE_MODE) |
169+
| `--image-max-tiles N` | maximum number of tiles for multi-tile vision models (e.g. Qwen3VL), overrides the<br/>value from the GGUF; use when the GGUF lacks the key or the model default is wrong<br/>for your model size (Qwen3VL defaults to 4, too low for 8B+ variants)<br/>(env: LLAMA_ARG_IMAGE_MAX_TILES) |
168170
| `--chat-template-kwargs STRING` | sets additional params for the json template parser, must be a valid json object string, e.g. '{"key1":"value1","key2":"value2"}'<br/>(env: LLAMA_CHAT_TEMPLATE_KWARGS) |
169171
| `--jinja, --no-jinja` | whether to use jinja template engine for chat (default: enabled)<br/>(env: LLAMA_ARG_JINJA) |
170172
| `--reasoning-format FORMAT` | controls whether thought tags are allowed and/or extracted from the response, and in which format they're returned; one of:<br/>- none: leaves thoughts unparsed in `message.content`<br/>- deepseek: puts thoughts in `message.reasoning_content`<br/>- deepseek-legacy: keeps `<think>` tags in `message.content` while also populating `message.reasoning_content`<br/>(default: auto)<br/>(env: LLAMA_ARG_THINK) |

tools/mtmd/clip.cpp

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1416,8 +1416,26 @@ struct clip_model_loader {
14161416
hparams.image_resize_algo = RESIZE_ALGO_BILINEAR;
14171417
get_u32(KEY_SPATIAL_MERGE_SIZE, hparams.n_merge, false);
14181418
get_u32(KEY_WIN_ATTN_PATTERN, hparams.n_wa_pattern, model.proj_type == PROJECTOR_TYPE_QWEN25VL); // only 2.5 requires it
1419-
// optional multi-tile cap; absent in GGUF → stays 0 and the qwen3vl preprocessor falls back to 4
1419+
// SigLIP2-Large ViT (n_head=16, n_layer=24) is used for 2B/4B — max_tiles=4 confirmed.
1420+
// Larger ViT variants (8B+) use SigLIP2-SO-400M with unknown max_tiles.
1421+
// Read from GGUF if present; otherwise default to 4 and warn for unknown ViTs.
1422+
hparams.preproc_max_tiles = 4;
1423+
const bool has_max_tiles_key = gguf_find_key(ctx_gguf.get(), KEY_PREPROC_MAX_TILES) >= 0;
14201424
get_u32(KEY_PREPROC_MAX_TILES, hparams.preproc_max_tiles, false);
1425+
if (has_max_tiles_key && (hparams.preproc_max_tiles < 1 || hparams.preproc_max_tiles > 256)) {
1426+
LOG_WRN("%s: clip.vision.preproc_max_tiles=%d out of range [1,256] in GGUF — defaulting to 4\n",
1427+
__func__, hparams.preproc_max_tiles);
1428+
hparams.preproc_max_tiles = 4;
1429+
}
1430+
if (!has_max_tiles_key && (hparams.n_head > 16 || hparams.n_layer > 24)) {
1431+
const char * model_name =
1432+
model.proj_type == PROJECTOR_TYPE_QWEN2VL ? "Qwen2VL" :
1433+
model.proj_type == PROJECTOR_TYPE_QWEN25VL ? "Qwen2.5VL" : "Qwen3VL";
1434+
LOG_WRN("%s: %s large ViT detected (n_head=%d, n_layer=%d) but "
1435+
"clip.vision.preproc_max_tiles not in GGUF — defaulting to 4. "
1436+
"If using an 8B+ model, set the correct value with --image-max-tiles.\n",
1437+
__func__, model_name, hparams.n_head, hparams.n_layer);
1438+
}
14211439
// ref: https://huggingface.co/Qwen/Qwen2.5-VL-7B-Instruct/blob/main/preprocessor_config.json
14221440
hparams.set_limit_image_tokens(8, 4096);
14231441
hparams.warmup_image_size = hparams.image_size; // warmup at actual tile size to match inference graph shape
@@ -2744,6 +2762,18 @@ struct clip_init_result clip_init(const char * fname, struct clip_context_params
27442762
if (loader.has_vision) {
27452763
ctx_vision = new clip_ctx(ctx_params);
27462764
loader.load_hparams(ctx_vision->model, CLIP_MODALITY_VISION);
2765+
// apply CLI overrides after load_hparams so GGUF defaults don't clobber them
2766+
if (ctx_params.image_max_tiles != -1) {
2767+
ctx_vision->model.hparams.preproc_max_tiles = ctx_params.image_max_tiles;
2768+
LOG_INF("%s: preproc_max_tiles: %d (custom value)\n", __func__, ctx_vision->model.hparams.preproc_max_tiles);
2769+
// models with a min-tiles floor (e.g. InternVL) would produce an empty candidate
2770+
// set if the override drops max below min; clamp min so the invariant holds.
2771+
if (ctx_vision->model.hparams.preproc_min_tiles > ctx_vision->model.hparams.preproc_max_tiles) {
2772+
LOG_WRN("%s: --image-max-tiles=%d is below the model's preproc_min_tiles=%d; clamping min\n",
2773+
__func__, ctx_params.image_max_tiles, ctx_vision->model.hparams.preproc_min_tiles);
2774+
ctx_vision->model.hparams.preproc_min_tiles = ctx_vision->model.hparams.preproc_max_tiles;
2775+
}
2776+
}
27472777
loader.load_tensors(*ctx_vision);
27482778
if (ctx_params.warmup) {
27492779
loader.warmup(*ctx_vision);

tools/mtmd/clip.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ struct clip_context_params {
5151
ggml_backend_sched_eval_callback cb_eval;
5252
void * cb_eval_user_data;
5353
const char * backend_device; // optional, if null will use env var or default GPU backend
54-
int image_tile_mode; // 0=batched (default), 1=sequential, 2=disabled
54+
int image_tile_mode; // 0=batched (default), 1=sequential, 2=disabled
55+
int image_max_tiles; // override preproc_max_tiles; -1 = use GGUF/model default
5556
};
5657

5758
struct clip_init_result {

tools/mtmd/mtmd-cli.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,8 @@ struct mtmd_cli_context {
143143
mparams.warmup = params.warmup;
144144
mparams.image_min_tokens = params.image_min_tokens;
145145
mparams.image_max_tokens = params.image_max_tokens;
146-
mparams.image_tile_mode = (int)params.image_tile_mode;
146+
mparams.image_tile_mode = (int)params.image_tile_mode;
147+
mparams.image_max_tiles = params.image_max_tiles;
147148
if (std::getenv("MTMD_DEBUG_GRAPH") != nullptr) {
148149
mparams.cb_eval_user_data = &cb_data;
149150
mparams.cb_eval = common_debug_cb_eval;

tools/mtmd/mtmd-image.cpp

Lines changed: 75 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -935,39 +935,69 @@ bool mtmd_image_preprocessor_qwen3vl::preprocess(const clip_image_u8 & img, clip
935935
GGML_ASSERT(tile_px > 0 && "image_size not set in model hparams");
936936

937937
GGML_ASSERT(hparams.patch_size > 0);
938+
// Guard the log-ratio division below; the public API asserts this in mtmd.cpp, but a direct
939+
// caller (fuzz harness, unit test, malformed upload reaching this path) could reach here with
940+
// a zero-dimension image. Fail this one image gracefully instead of aborting the whole process.
941+
if (img.nx <= 0 || img.ny <= 0) {
942+
LOG_ERR("%s: image has zero dimension (%dx%d)\n", __func__, img.nx, img.ny);
943+
return false;
944+
}
938945

939-
// Pick the grid (n_col × n_row with n_col*n_row <= max_tiles, excluding 1×1) whose aspect
940-
// ratio is closest to the image's (log-ratio distance), minimising the stretch needed to
941-
// fill the tile canvas. Using log-ratio so e.g. 2:1 and 1:2 are symmetric around 1:1.
942-
std::vector<clip_image_size> candidate_grids;
946+
// Select the grid (n_col × n_row, n_col*n_row <= max_tiles, excluding 1×1) that:
947+
// 1. downscales the image in both dimensions (upscaling gains no detail)
948+
// 2. maximises tile count (more tiles = more spatial detail preserved)
949+
// 3. among equal tile counts, minimises log-ratio error (less stretch distortion)
950+
// If no grid downscales, fall back to dyn_size (single-tile at native resolution).
951+
const float img_log_ratio = std::log((float)img.nx / (float)img.ny);
952+
953+
struct grid_cand { int col, row; float ratio_err; };
954+
std::vector<grid_cand> fitting;
955+
// candidate count is sum_{col} floor(max_tiles/col) ≈ max_tiles*ln(max_tiles), not max_tiles²;
956+
// a small reserve avoids the early reallocations without over-allocating at large max_tiles.
957+
fitting.reserve((size_t)(max_tiles * std::log((float)std::max(max_tiles, 2))));
943958
for (int col = 1; col <= max_tiles; col++) {
944959
for (int row = 1; col * row <= max_tiles; row++) {
945-
if (col == 1 && row == 1) { continue; } // 1×1 == the dyn_size fallback below
946-
candidate_grids.push_back({col, row});
960+
if (col == 1 && row == 1) { continue; } // 1×1 == dyn_size below
961+
if (col * tile_px <= img.nx && row * tile_px <= img.ny) {
962+
const float err = std::abs(img_log_ratio - std::log((float)col / (float)row));
963+
fitting.push_back({col, row, err});
964+
}
947965
}
948966
}
949-
const float img_log_ratio = std::log((float)img.nx / (float)img.ny);
950-
const clip_image_size best_grid = pick_grid_by_log_ratio(candidate_grids, img_log_ratio);
951-
const int best_col = best_grid.width;
952-
const int best_row = best_grid.height;
953-
954-
const int target_w = best_col * tile_px;
955-
const int target_h = best_row * tile_px;
956-
957-
// Only tile when the canvas DOWNSCALES the image. If the chosen tile canvas is larger than
958-
// the image in either dimension, tiling would upscale it — no real detail is gained, it just
959-
// spends several full-tile embeddings (plus an overview) on an image a single dyn_size pass
960-
// represents fully and more cheaply. This subsumes the old "fits in one tile" check (any such
961-
// image maps to a canvas that upscales it) and avoids the medium-image token blow-up, while
962-
// large images the canvas downscales still tile to preserve local detail.
963-
if (target_w > img.nx || target_h > img.ny) {
964-
LOG_INF("%s: %dx%d would upscale into a %dx%d tile canvas — using dyn_size instead\n",
965-
__func__, img.nx, img.ny, target_w, target_h);
967+
if (fitting.empty()) {
968+
LOG_INF("%s: %dx%d fits no tile grid (tile_px=%d, max_tiles=%d) — using dyn_size\n",
969+
__func__, img.nx, img.ny, tile_px, max_tiles);
966970
preprocess_dyn_size_aligned(img, output, hparams, *this, dyn_size_align_px(hparams));
967971
// grid_x/grid_y left at 0 → single-tile path; tokenizer treats this as 1×1
968972
return true;
969973
}
970974

975+
// Tolerance band: find the best (minimum) ratio error, then allow any candidate within
976+
// TOLERANCE of it to compete on tile count. This prevents a high-tile-count grid from
977+
// winning when a near-perfect-ratio lower-tile-count grid exists (e.g. 2×2 over 2×1
978+
// for a 2:1 image, which would squash the image 2× in one dimension).
979+
static constexpr float RATIO_ERR_TOLERANCE = 0.2f;
980+
const float best_ratio_err = std::min_element(fitting.begin(), fitting.end(),
981+
[](const grid_cand & a, const grid_cand & b) { return a.ratio_err < b.ratio_err; })->ratio_err;
982+
const float eligible_threshold = best_ratio_err + RATIO_ERR_TOLERANCE;
983+
984+
auto best = std::min_element(fitting.begin(), fitting.end(), [eligible_threshold](const grid_cand & a, const grid_cand & b) {
985+
const bool ea = a.ratio_err <= eligible_threshold;
986+
const bool eb = b.ratio_err <= eligible_threshold;
987+
if (ea != eb) { return ea; } // eligible always beats ineligible
988+
const int ta = a.col * a.row;
989+
const int tb = b.col * b.row;
990+
if (ta != tb) { return ta > tb; } // more tiles first
991+
return a.ratio_err < b.ratio_err; // then closer ratio
992+
});
993+
994+
const int use_col = best->col;
995+
const int use_row = best->row;
996+
LOG_INF("%s: %dx%d — selected %dx%d grid (%d tiles, ratio_err=%.3f)\n",
997+
__func__, img.nx, img.ny, use_col, use_row, use_col * use_row, best->ratio_err);
998+
const int target_w = use_col * tile_px;
999+
const int target_h = use_row * tile_px;
1000+
9711001
// Resize to the tile canvas by stretching. With aspect-ratio-aware grid selection the chosen
9721002
// grid's ratio is close to the image's ratio, so the residual stretch is small.
9731003
// pad=false avoids black bars that confuse the model in tile inputs.
@@ -977,11 +1007,28 @@ bool mtmd_image_preprocessor_qwen3vl::preprocess(const clip_image_u8 & img, clip
9771007
/* pad */ PAD_NONE,
9781008
hparams.image_pad_color);
9791009

980-
// Split into best_col × best_row tiles, packed row-major into the batch.
1010+
// LLaVA-style global overview (thumbnail): a downscaled full image (tile_px×tile_px) placed as
1011+
// entries[0]. It carries the whole image uncut, so text split across a tile seam stays readable;
1012+
// on DocVQA this recovers seam-truncated text for a sizeable ANLS gain. Encoded as a separate
1013+
// 1×1 chunk in mtmd.cpp; the tiles are untouched (no resolution cost to them). Built before the
1014+
// tiles so it lands at entries[0] via push_back, avoiding an O(N) insert-at-begin pointer shift.
1015+
{
1016+
clip_image_u8 thumb_u8;
1017+
img_tool::resize(img, thumb_u8, {tile_px, tile_px},
1018+
hparams.image_resize_algo,
1019+
/* pad */ PAD_NONE,
1020+
hparams.image_pad_color);
1021+
clip_image_f32_ptr thumb_f32(clip_image_f32_init());
1022+
img_u8_to_f32(thumb_u8, *thumb_f32, hparams.image_mean, hparams.image_std);
1023+
output.entries.push_back(std::move(thumb_f32));
1024+
output.has_overview = true;
1025+
}
1026+
1027+
// Split into use_col × use_row tiles, packed row-major into the batch after the thumbnail.
9811028
clip_image_u8 tile_u8;
9821029

983-
for (int tr = 0; tr < best_row; tr++) {
984-
for (int tc = 0; tc < best_col; tc++) {
1030+
for (int tr = 0; tr < use_row; tr++) {
1031+
for (int tc = 0; tc < use_col; tc++) {
9851032
const int src_x0 = tc * tile_px;
9861033
const int src_y0 = tr * tile_px;
9871034
img_tool::crop(resized, tile_u8, src_x0, src_y0, tile_px, tile_px);
@@ -992,24 +1039,8 @@ bool mtmd_image_preprocessor_qwen3vl::preprocess(const clip_image_u8 & img, clip
9921039
}
9931040
}
9941041

995-
// LLaVA-style global overview (thumbnail): a downscaled full image (tile_px×tile_px) prepended
996-
// as entries[0]. It carries the whole image uncut, so text split across a tile seam stays
997-
// readable; on DocVQA this recovers seam-truncated text for a sizeable ANLS gain. Encoded as a
998-
// separate 1×1 chunk in mtmd.cpp; the tiles are untouched (no resolution cost to them).
999-
{
1000-
clip_image_u8 thumb_u8;
1001-
img_tool::resize(img, thumb_u8, {tile_px, tile_px},
1002-
hparams.image_resize_algo,
1003-
/* pad */ PAD_NONE,
1004-
hparams.image_pad_color);
1005-
clip_image_f32_ptr thumb_f32(clip_image_f32_init());
1006-
img_u8_to_f32(thumb_u8, *thumb_f32, hparams.image_mean, hparams.image_std);
1007-
output.entries.insert(output.entries.begin(), std::move(thumb_f32));
1008-
output.has_overview = true;
1009-
}
1010-
1011-
output.grid_x = best_col;
1012-
output.grid_y = best_row;
1042+
output.grid_x = use_col;
1043+
output.grid_y = use_row;
10131044
return true;
10141045
}
10151046

tools/mtmd/mtmd.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ mtmd_context_params mtmd_context_params_default() {
124124
/* cb_eval_user_data */ nullptr,
125125
/* backend_device */ nullptr,
126126
/* image_tile_mode */ 1, // 0=batched, 1=sequential (default), 2=disabled
127+
/* image_max_tiles */ -1,
127128
};
128129
return params;
129130
}
@@ -195,6 +196,7 @@ struct mtmd_context {
195196
/* cb_eval_user_data */ ctx_params.cb_eval_user_data,
196197
/* backend_device */ ctx_params.backend_device,
197198
/* image_tile_mode */ ctx_params.image_tile_mode,
199+
/* image_max_tiles */ ctx_params.image_max_tiles,
198200
};
199201

200202
auto res = clip_init(mmproj_fname, ctx_clip_params);

tools/mtmd/mtmd.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ struct mtmd_context_params {
102102

103103
// tile encoding mode for multi-tile vision models (Qwen3VL): 0=batched (default), 1=sequential, 2=disabled
104104
int image_tile_mode;
105+
106+
// override preproc_max_tiles from GGUF; -1 = use model default (4 for Qwen3VL 2B/4B)
107+
// needed for 8B+ models whose GGUFs may lack the clip.vision.preproc_max_tiles key
108+
int image_max_tiles;
105109
};
106110

107111
MTMD_API const char * mtmd_default_marker(void);

tools/server/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,8 @@ For the full list of features, please refer to [server's changelog](https://gith
181181
| `--mmproj-offload, --no-mmproj-offload` | whether to enable GPU offloading for multimodal projector (default: enabled)<br/>(env: LLAMA_ARG_MMPROJ_OFFLOAD) |
182182
| `--image-min-tokens N` | minimum number of tokens each image can take, only used by vision models with dynamic resolution (default: read from model)<br/>(env: LLAMA_ARG_IMAGE_MIN_TOKENS) |
183183
| `--image-max-tokens N` | maximum number of tokens each image can take, only used by vision models with dynamic resolution (default: read from model)<br/>(env: LLAMA_ARG_IMAGE_MAX_TOKENS) |
184+
| `--image-tile-mode MODE` | tile encoding mode for multi-tile vision models (e.g. Qwen3VL):<br/> batched - all tiles in one forward pass<br/> sequential - tiles encoded one-by-one (default)<br/> disabled - tiling disabled, single tile only<br/>(env: LLAMA_ARG_IMAGE_TILE_MODE) |
185+
| `--image-max-tiles N` | maximum number of tiles for multi-tile vision models (e.g. Qwen3VL), overrides the<br/>value from the GGUF; use when the GGUF lacks the key or the model default is wrong<br/>for your model size (Qwen3VL defaults to 4, too low for 8B+ variants)<br/>(env: LLAMA_ARG_IMAGE_MAX_TILES) |
184186
| `-a, --alias STRING` | set model name aliases, comma-separated (to be used by API)<br/>(env: LLAMA_ARG_ALIAS) |
185187
| `--tags STRING` | set model tags, comma-separated (informational, not used for routing)<br/>(env: LLAMA_ARG_TAGS) |
186188
| `--embd-normalize N` | normalisation for embeddings (default: 2) (-1=none, 0=max absolute int16, 1=taxicab, 2=euclidean, >2=p-norm) |

0 commit comments

Comments
 (0)