File size: 4,223 Bytes
2b6ab7c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
#!/usr/bin/env python3
"""Run a command while sampling how the model file is actually read.

Reports, per sample and at the end:
  read_bytes   /proc/<pid>/io          bytes actually fetched from the block device
  file_resident  mincore(model file)   pages of the GGUF sitting in the page cache
  RssAnon/RssFile                     process RSS split (arena+weights vs mapped file)
  Cached          /proc/meminfo       system-wide page cache

The point: a buffered read would show up as file_resident growing; O_DIRECT reads
never touch the cache, so read_bytes grows while file_resident stays flat.

usage: watch_io.py <model.gguf>[,<model2.gguf>,...] <cmd> [args...]
"""
import os
import re
import subprocess
import sys
import time

PAGE = os.sysconf('SC_PAGE_SIZE')
CHR = 1024 ** 3


def meminfo(key):
    for line in open('/proc/meminfo'):
        if line.startswith(key):
            return int(line.split()[1]) * 1024
    return 0


def proc_io(pid):
    out = {}
    try:
        for line in open(f'/proc/{pid}/io'):
            k, _, v = line.partition(':')
            out[k] = int(v)
    except OSError:
        return None
    return out


def proc_status(pid):
    out = {}
    try:
        for line in open(f'/proc/{pid}/status'):
            for k in ('RssAnon', 'RssFile', 'VmHWM'):
                if line.startswith(k):
                    out[k] = int(line.split()[1]) * 1024
    except OSError:
        pass
    return out


def file_resident(paths):
    """resident bytes of the given files via mincore (helper binary)"""
    r = subprocess.run(['/tmp/pagecache_resident'] + paths, capture_output=True, text=True)
    m = re.search(r'TOTAL\s+resident\s+([\d.]+) GiB', r.stdout)
    if m:
        return float(m.group(1)) * CHR
    m = re.search(r'resident\s+([\d.]+) GiB', r.stdout)
    return float(m.group(1)) * CHR if m else -1


def main():
    models = sys.argv[1].split(',')
    cmd = sys.argv[2:]
    subprocess.run(['sync'])
    print(f'[watch] evicting {len(models)} shard(s) from the page cache', flush=True)
    subprocess.run(['/tmp/pagecache_resident', '--evict'] + models)

    base_cached = meminfo('Cached:')
    base_read = None
    t0 = time.time()
    # NOTE: never use pipes here - sdq-chat prints several hundred KB of
    # tokenizer warnings and would block on a full 64 KB pipe.
    out_f = open('/tmp/watch_stdout.log', 'w')
    err_f = open('/tmp/watch_stderr.log', 'w')
    proc = subprocess.Popen(cmd, stdout=out_f, stderr=err_f)
    pid = proc.pid

    peak_res = 0.0
    peak_rssanon = peak_rssfile = 0
    peak_cached = base_cached
    samples = 0
    while proc.poll() is None:
        io_ = proc_io(pid)
        st = proc_status(pid)
        if io_ is None:
            break
        if base_read is None:
            base_read = io_['read_bytes']
        res = file_resident(models)
        cached = meminfo('Cached:')
        peak_res = max(peak_res, res)
        peak_cached = max(peak_cached, cached)
        peak_rssanon = max(peak_rssanon, st.get('RssAnon', 0))
        peak_rssfile = max(peak_rssfile, st.get('RssFile', 0))
        samples += 1
        print(f'[watch] t={time.time()-t0:6.1f}s  read_bytes={(io_["read_bytes"]-base_read)/CHR:7.3f} GiB '
              f' rchar={(io_["rchar"])/CHR:8.3f} GiB  model_file_resident={res/CHR:6.3f} GiB  '
              f'RssAnon={st.get("RssAnon",0)/CHR:6.3f}  RssFile={st.get("RssFile",0)/CHR:6.3f}  '
              f'Cached_delta={(cached-base_cached)/CHR:+7.3f} GiB', flush=True)
        time.sleep(float(os.environ.get("WATCH_INTERVAL", "2.0")))

    proc.wait()
    out_f.close()
    err_f.close()
    print(f'\n[watch] === summary over {samples} samples, {time.time()-t0:.1f}s ===')
    print(f'[watch] peak model_file_resident in page cache : {peak_res/CHR:.3f} GiB')
    print(f'[watch] peak process RssAnon                  : {peak_rssanon/CHR:.3f} GiB')
    print(f'[watch] peak process RssFile                  : {peak_rssfile/CHR:.3f} GiB')
    print(f'[watch] system Cached delta (peak)            : {(peak_cached-base_cached)/CHR:+.3f} GiB')
    print(f'[watch] final model_file_resident             : {file_resident(models)/CHR:.3f} GiB')
    return 0


if __name__ == '__main__':
    sys.exit(main())