‹ BackHN Continuity

Thread

Can gzip be a language model?

414 points · 165 comments · networked

  1. networked · · focus · HN ↗
    I was curious to see how this would work with bzip2 and zstd. The source is public at <a href="https:&#x2F;&#x2F;github.com&#x2F;nathanrs&#x2F;gzipt" rel="nofollow">https:&#x2F;&#x2F;github.com&#x2F;nathanrs&#x2F;gzipt, and I asked MiMo-V2.6-Flash to fork and modify it in a straightforward way. The answer is that bzip2 produces sequences that don&#x27;t resemble human language:

      gzipt \
          --corpus data&#x2F;tinyshakespeare.txt \  
          --prompt $&#x27;MENENIUS:\n&#x27; \
          --length 200 \
          ;
      
      MENENIUS:
      MtLUMSeptuttyyyxyxyxyxyvyyyxyxyxyxyvyyyxyxyxyxywyvzyxyxyx
      yyxyyyxyxyxyxyxPlyxyxyxyxyxyxyxyxyxtoxzfTUS.zxzzzyzzzvzzz
      vzzzxvzyvyxyxyxyvyxyxyxyvy--,Vdvyxyxyxyxyxyxyxyxyxxy!zFlx
      zzyyxyxyxyvyxyxyxyvyySPffuyuy
    
    Line breaks added. This looks roughly optimized for the most repetitive Burrows-Wheeler transform (<a href="https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Burrows%E2%80%93Wheeler_transform" rel="nofollow">https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Burrows%E2%80%93Wheeler_transf...). Why are they runs of alternating symbols and not one symbol?

    Zstandard produces whitespace with the occasional letter thrown in. To quote MiMo: &quot;As you can see, zstd does not speak Shakespeare. ... zstd encodes a run of one repeated byte as a near-free run-length sequence, and space and newline are the cheapest literals in the corpus: ten newlines cost about the same to append ten bytes of genuine corpus text and less than nonsense does.&quot;

    1. maxidog · · focus · HN ↗
      Did you check MiMo correctly performed this unfamiliar task before posting this comment?
      1. networked · · focus · HN ↗
        I did. I read the code to make sure the quality of MiMo&#x27;s work matched mine for a quick experiment, though not that the code was free from subtle bugs.

        This was the main change for bzip2:

          @@ -33,19 +34,16 @@ def candidate_lengths(
               level: int = 9,
               pool: ThreadPoolExecutor | None = None,
           ) -&gt; list[int]:
          -    &quot;&quot;&quot;Compressed length of ``context + seq`` for each seq, sharing the context.
          +    &quot;&quot;&quot;Compressed length of ``context + seq`` for each seq.
          
          -    Compresses ``context`` once into a ``compressobj``, then clones its encoder
          -    state per candidate and feeds only that candidate. Identical to
          -    ``len(zlib.compress(context + seq, level))`` for each seq, but the expensive
          -    match search over ``context`` happens a single time.
          +    Unlike ``zlib``&#x27;s ``compressobj``, Python&#x27;s ``BZ2Compressor`` cannot be
          +    snapshotted mid-stream, and bzip2&#x27;s move-to-front + Huffman stages see the
          +    whole block, so every candidate recompresses the full context. Threads
          +    still scale because ``bz2`` releases the GIL.
               &quot;&quot;&quot;
          -    base = zlib.compressobj(level)
          -    head = len(base.compress(context))
          
               def length_for(seq: bytes) -&gt; int:
          -        clone = base.copy()
          -        return head + len(clone.compress(seq) + clone.flush(zlib.Z_FINISH))
          +        return len(bz2.compress(context + seq, level))
          
               if pool is not None:
                   return list(pool.map(length_for, sequences))
        1. jrmg · · focus · HN ↗
          Is the fact that the original did [compress base]+[compress seq] rather than [compress [bytes + seq]] not important?

          (I honestly don’t know is gzip does something different when presented with two chunks as opposed to one, or, if it does, if bz2 has equivalent behaviour - but the difference in the code did stand out to me, and it does seem related to ‘extending the token sequence’)

          1. networked · · focus · HN ↗
            This difference doesn&#x27;t matter because of how zlib works. At least by default, zlib divides the input data into its own blocks independent of the caller. If you don&#x27;t feed it enough data to complete a block, it waits until you feed it more or finish the stream.

            We can test it by going back to zlib:

                   def length_for(seq: bytes) -&gt; int:
              -        return len(bz2.compress(context + seq, level))
              +        return len(zlib.compress(context + seq, level))
            
            At temperature zero, this outputs the same sample as commit 3734bf6, the most recent commit upstream:

              MENENIUS:
              &#x27;Though all at once cannq
            
              MARCIUS:
              I&#x27;ll fight
              &#x27;Though all at once cannq
            
              MARCIUannq
              
              MARCIUS:
              I&#x27;ll fight
              &#x27;Though
              
              AUFIDIUS:
              If I fly, Marci
              
              AUFIDIUS:
              If I fly, Marci
              
              AUFID
              
              AUFIDIUS:
              If
              If I fly
            
            I also tried LZMA for good measure:

                   def length_for(seq: bytes) -&gt; int:
              -        return len(bz2.compress(context + seq, level))
              +        return len(lzma.compress(context + seq))
            
            The sample at temperature zero:

              MENENIUS:
              &#x27;Th
              
              A carbuncle enti
              
              , as big as thou
              
              
              A aa
            
            This is followed by a lot of whitespace.

            python-lz4 gives you all newlines after the prompt. I tried debugging it, and the compressed length of different candidate seqs is the same.

Open on Hacker News to reply ↗

Unofficial Hacker News client; not affiliated with Y Combinator.