@@ -393,9 +393,54 @@ def consider(directory: str, name: str) -> None:
393393 return found
394394
395395
396+ ARCH_KEYS = (
397+ "block_count" ,
398+ "embedding_length" ,
399+ "context_length" ,
400+ "attention.head_count" ,
401+ "attention.head_count_kv" ,
402+ "attention.key_length" ,
403+ "attention.value_length" ,
404+ )
405+
406+ KV_ELEMENT_BYTES = 2
407+
396408_GGUF_HEADER_CACHE : dict [tuple [str , int , int ], dict ] = {}
397409
398410
411+ def _header_int (value ) -> int | None :
412+ """One integer out of a header value that is sometimes a per-layer array."""
413+ if isinstance (value , (list , tuple )):
414+ value = max (value ) if value else None
415+ try :
416+ return int (value )
417+ except (TypeError , ValueError ):
418+ return None
419+
420+
421+ def _kv_per_token (value , arch : str , blocks : int | None , width : int | None ) -> int | None :
422+ """Bytes one token costs in the KV cache, or ``None`` from a header too thin.
423+
424+ ``key_length`` and ``value_length`` are optional and often absent; the head
425+ dimension is then ``embedding_length / head_count``, which is the same
426+ fallback llama.cpp makes. ``head_count_kv`` missing means the model has no
427+ grouped-query attention and every head keeps its own cache.
428+ """
429+ if not arch or not blocks :
430+ return None
431+ heads = _header_int (value (f"{ arch } .attention.head_count" ))
432+ kv_heads = _header_int (value (f"{ arch } .attention.head_count_kv" )) or heads
433+ if not kv_heads :
434+ return None
435+ key_length = _header_int (value (f"{ arch } .attention.key_length" ))
436+ if key_length is None and heads and width :
437+ key_length = width // heads
438+ value_length = _header_int (value (f"{ arch } .attention.value_length" )) or key_length
439+ if not key_length or not value_length :
440+ return None
441+ return blocks * kv_heads * (key_length + value_length ) * KV_ELEMENT_BYTES
442+
443+
399444def gguf_header (path : str ) -> dict :
400445 """Architecture, type and shape of a GGUF, read from its header only.
401446
@@ -407,7 +452,10 @@ def gguf_header(path: str) -> dict:
407452 Cached per file identity, so a folder of large quants costs no more than a
408453 stat each after the first pass.
409454 """
410- empty = {"arch" : "" , "kind" : "" , "blocks" : None , "width" : None , "vision" : False , "audio" : False }
455+ empty = {
456+ "arch" : "" , "kind" : "" , "blocks" : None , "width" : None ,
457+ "vision" : False , "audio" : False , "context" : None , "kv_per_token" : None ,
458+ }
411459 try :
412460 stat = os .stat (path )
413461 except OSError :
@@ -423,7 +471,7 @@ def gguf_header(path: str) -> dict:
423471
424472 def also (found : dict ) -> tuple [str , ...]:
425473 arch = found .get ("general.architecture" )
426- return (f"{ arch } .block_count" , f" { arch } .embedding_length" ) if arch else ()
474+ return tuple (f"{ arch } .{ name } " for name in ARCH_KEYS ) if arch else ()
427475
428476 value = gguf_meta .keys (path , HEADER_KEYS , probe = also , verify = True ).get
429477
@@ -438,6 +486,10 @@ def also(found: dict) -> tuple[str, ...]:
438486 width = value (f"{ header ['arch' ]} .embedding_length" )
439487 header ["blocks" ] = int (blocks ) if blocks is not None else None
440488 header ["width" ] = int (width ) if width is not None else None
489+ header ["context" ] = _header_int (value (f"{ header ['arch' ]} .context_length" ))
490+ header ["kv_per_token" ] = _kv_per_token (
491+ value , header ["arch" ], header ["blocks" ], header ["width" ]
492+ )
441493 except Exception :
442494 log .debug ("[minimax_h3_rewriter.gguf_header] %s unreadable" , path , exc_info = True )
443495
@@ -653,20 +705,18 @@ def scan_captioner_gguf(arch: str | None = None) -> list[tuple[str, str, str]]:
653705 elif header ["kind" ] == "model" :
654706 models .append (path )
655707
656- for _directory , (models , projectors ) in sorted (by_directory .items ()):
708+ for directory , (models , projectors ) in sorted (by_directory .items ()):
657709 if not projectors :
658710 continue
711+ unpaired : list [str ] = []
659712 for model in sorted (models ):
660713 # After counting the models, not before: how many there are is what
661714 # decides whether a lone projector in the folder can be trusted.
662715 if arch is not None and gguf_header (model )["arch" ] != arch :
663716 continue
664717 projector = _pair_mmproj (model , projectors , len (models ))
665718 if not projector :
666- log .info (
667- "[minimax_h3_rewriter.scan_captioner_gguf] no obvious projector for %s among %s" ,
668- model , [os .path .basename (p ) for p in projectors ],
669- )
719+ unpaired .append (os .path .basename (model ))
670720 continue
671721 header = gguf_header (projector )
672722 modalities = ", " .join (
@@ -678,5 +728,11 @@ def scan_captioner_gguf(arch: str | None = None) -> list[tuple[str, str, str]]:
678728 size = 0.0
679729 label = f"{ os .path .basename (model )} [+mmproj, { modalities } , { size :.1f} GB]"
680730 found .append ((label , model , projector ))
731+ if unpaired :
732+ log .debug (
733+ "[minimax_h3_rewriter.scan_captioner_gguf] %s: %d model(s) with no projector "
734+ "of their own among %s: %s" ,
735+ directory , len (unpaired ), [os .path .basename (p ) for p in projectors ], unpaired ,
736+ )
681737
682738 return found
0 commit comments