# -*- coding: utf-8 -*-
"""【估算 · 嵌入式设备可行性】给定内存预算,函数化 + 量化能装下多大模型。

一句话结论:
    本库能把「大模型上嵌入式」的体积/内存这堵墙撬动,但它只做「压缩 + 生成物」,
    不含推理运行时——产物要能在嵌入式跑,必须有一个 C/Rust 推理 kernel 去消费它。
    本例按「只算权重存储」的口径,把函数化 + 量化(f32→i8→i4)能压出的体积,
    对照三档设备(MCU / 边缘 SoC / 64MB 中间档)的内存预算逐行判「装得下 / 装不下」。

对应 README「嵌入式设备可行性」节的估算表。口径与 README 一致:
    - f32 体积 = 整个模型参数量 × 4 字节(本库处理对象是「一层权重」,但本节
      估的是「整个模型」的上嵌入式体积,故按模型总参数算);
    - 函数化压缩比:约 7~17×(取决于 K/n),本节统一取保守中值 12×;
    - 再叠加量化位宽:i8=÷4、i4=÷8(相对 f32),f32 函数化=÷12。

这是【估算】,不是【实测】:体积是确定性算术,此处给「装得下」口径;
「跑得动」与「不掉点」是另外两堵更硬的墙,本例只在结尾如实标注边界。

诚实边界(必须记住):
    ① 上表只算权重、不含激活缓冲(嵌入式运行时激活常比权重更吃内存);
    ② 掉点口径:跨规模 MLP 扫描的实测(scale_scan.py)+ 真实 ResNet50 函数化验证;
       真实大 CNN/ResNet50 层函数化掉点可压到 ±0.5pt,但小层(h=64)叠加掉点仍会到
       4.16pt——「装得下」与「不掉点」都取决于层冗余度,本例不代测掉点;
    ③ 「装得下」≠「跑得动」,推理框架与算力是另一堵更硬的墙。
"""
import _common  # noqa: F401  # 先导入:完成 src 路径引导


# ── 模型规格(参数量 → 整个模型的 f32 体积)──
#   函数化压缩比统一取 12×(README「约 7~17 倍」的保守中值)。
#   注意:这是「体积算术」的经验口径,真实压缩比逐层看 K/n(见 scale_scan.py),
#   这里不逐模型造不同压缩比,避免引入主观拟合。
MODELS = [
    ('小 MLP(0.5M)', 0.5e6),
    ('中 CNN(5M)', 5e6),
    ('较大 CNN(25M)', 25e6),
    ('大 ViT(100M)', 100e6),
]

# 函数化统一压缩比(README「约 7~17 倍」的保守中值)
FUNC_COMPRESS = 12.0

# 三档内存预算(MB)
BUDGETS_MB = [512, 64, 8]


def _mb(params):
    """参数量 → f32 字节 → MB。"""
    return params * 4 / (1024 ** 2)


def _region_mb(f32_mb, bits):
    """函数化后体积:f32 体积 ÷ 函数化压缩比 ÷ 量化压缩比。

    bits=32 → 与 f32 同口径(仅函数化 ÷12);8 → ÷4;4 → ÷8。
    """
    quant = 32 // bits  # 32→1, 8→4, 4→8
    return f32_mb / FUNC_COMPRESS / quant


def estimate_embedded():
    print('=' * 88)
    print('嵌入式设备可行性估算(只算权重存储,不含激活缓冲)')
    print('=' * 88)
    print('口径:')
    print('  · f32 体积 = 整个模型参数量 × 4 字节;')
    print('  · 函数化压缩比取 12×(README「约 7~17 倍」保守中值,统一口径);')
    print('  · 再叠加量化:i8=÷4、i4=÷8(相对 f32)。')
    print()

    # 主表:模型 × 预算,选「最省口径(i4 函数化)」判装得下
    print(f"{'模型':<18}{'f32体积':>12}" + ''.join(
        f"{f'预算{b}MB':>14}" for b in BUDGETS_MB))
    print('-' * (18 + 12 + 14 * len(BUDGETS_MB)))
    for name, params in MODELS:
        f32_mb = _mb(params)
        i4_mb = _region_mb(f32_mb, 4)
        marks = ''.join(
            f"{('✓' if i4_mb <= b else '✗'):>14}" for b in BUDGETS_MB)
        print(f'{name:<18}{f32_mb:>11.1f} MB{marks}')
    print('-' * (18 + 12 + 14 * len(BUDGETS_MB)))
    print()

    # 明细:每个模型在 f32 / i8 / i4 三种函数化口径下的体积
    print(f"{'模型':<18}{'f32':>12}{'f32函数化':>12}{'i8函数化':>12}{'i4函数化':>12}")
    print('-' * 68)
    for name, params in MODELS:
        f32 = _mb(params)
        print(f'{name:<18}{f32:>11.2f} MB{_region_mb(f32, 32):>11.2f} MB'
              f'{_region_mb(f32, 8):>11.2f} MB{_region_mb(f32, 4):>11.2f} MB')
    print('-' * 68)
    print()

    # 结论(与算出的体积严格一致,不照抄旧 README 文案)
    print('逐行结论(「装得下」判定,取最省 i4 口径):')
    for name, params in MODELS:
        f32 = _mb(params)
        i4 = _region_mb(f32, 4)
        i8 = _region_mb(f32, 8)
        # 结论从算出的数字推导,不自相矛盾
        if f32 <= 8:
            note = '轻松(f32 就装得下)'
        elif i8 <= 8:
            note = f'轻松(i8 函数化 {i8:.2f}MB 即装下)'
        elif i4 <= 8:
            note = f'仅 i4 能过(i4 函数化 {i4:.2f}MB)'
        else:
            note = '装不下'
        print(f'  · {name}:f32 {f32:.1f}MB → i4 函数化 {i4:.2f}MB,{note}')
    print()

    _print_honesty()


def _print_honesty():
    print('=' * 88)
    print('三档设备分水岭(统一 12× 函数化压缩比口径):')
    print('  · MCU 级(STM32/ESP32,KB~MB RAM):基本不可行——即便 i4 压进体积,')
    print('    MCU 也没有算力跑它,本库只能当「存储端压缩」。')
    print('  · 边缘 SoC(RK3588/树莓派,GB 级):最佳落点——中模型轻松装下、')
    print('    且「又省又稳」。')
    print('  · 中间档(64MB / 8MB 级):25M 参数 i8 即过 64MB,100M 需 i4 才稳妥。')
    print()
    print('诚实边界(必须一起读,别只看「装得下」的 ✓):')
    print('  ① 上表只算权重、不含激活缓冲(嵌入式运行时激活常比权重更吃内存)。')
    print('  ② 掉点口径:跨规模 MLP 扫描的实测(scale_scan.py)+ 真实 ResNet50 函数化验证;真实大')
    print('    CNN/ResNet50 层函数化掉点可压到 ±0.5pt,但小层(h=64)叠加掉点仍会到 4.16pt——')
    print('    「装得下」与「不掉点」都取决于层冗余度,本例不代测掉点。')
    print('  ③ 「装得下」≠「跑得动」,推理框架与算力是另一堵更硬的墙。')
    print('  ④ 本库只做「压缩 + 生成物」,不含推理运行时——产物要能在嵌入式跑,')
    print('    必须有一个 C/Rust 推理 kernel 去消费它。')
    print('=' * 88)


def main():
    estimate_embedded()


if __name__ == '__main__':
    main()