Game Over: How Nintendo Conquered The World

Game Over: How Nintendo Conquered The World 作者是David Sheff,中文译本名字 《游戏结束:任天堂全球征服史》。

很有趣的一本书,以任天堂的发家扩张为主线,讲到了很多游戏和游戏公司的往事。可能是因为历史本身比较精彩,直接讲出来就已经是很吸引人的故事了。

作者成书于1993年,那个时候正值老任高光时刻。每年的税前利润已经大于10亿美元,比当时美国所有的电影片商和三大电视网络公司加起来还多。当时出新主机的时候,抢购的狂热程度和现在也没两样,瞬间售罄,黄牛倒卖,无良商贩打包其他乱七八糟的游戏出售。我看到这段的时候就想到上次排队抢 switch,最后也是不得以被 GameStop 卖了个 Bundle。

挺有趣的事情是,其实任天堂最早是卖一种日本纸牌的,叫花札 Hanafuda。当时的创始人叫 山内 (Yamauchi) 房治郎。任天堂的名字也挺有意思,翻译过来就是“听天命”。书上英文描述了半天 (deep in the mind we have to do whatever we have to do),感觉还是中文比较直接,还挺佛系的。前期的老任就靠纸牌赚得也是不少,但也就只是一家纸牌公司,市场体量有限。直到山内的曾孙山内溥 (Hiroshi Yamauchi) 在1949年接手任天堂之后,才让这家公司一举成为世界级的游戏公司。

山内溥是任天堂发展史上的重要人物,书中对他的描写也颇为戏剧化。前期基本上是一个离经叛道的青年,时常面无表情,不讨人喜欢。接手公司之后,公司的老员工也不喜欢他。于是他就开始清洗前朝老臣…山内溥虽然是暴君,但是很有能力。他和迪士尼合作,把迪士尼的大IP米老鼠引入他们家的纸牌,从而大赚了一笔。然后他就一直在想怎么从纸牌这个市场跳出去做更大的事情,把最初公司名字里面带的纸牌的词都去掉了,决心要做新的市场。为了筹钱开展业务,还把公司上市了。

后来就是一直讲他试过的各种乱七八糟的事情,比如个人份的方便米饭(不知道是个啥),然后居然还有 love hotel 钟点房,也算是很先锋的思路了。他还开了出租车公司。试了一圈之后都没用,才决定还是要做和本行相关的东西,也就是娱乐行业 (entertainment)。

只一个人肯定不能成事,于是接下来就是山内溥麾下豪杰逐个登场。比如横井军平 (Gunpei Yokoi) 带领R&D1,做出来了当年横扫全球的Game Boy。上村雅之带领R&D2做出来了NES也就是红白机,国内的小霸王游戏机就都是NES的游戏。R&D2还有个有名的人物叫青沼英二 (Eiji Aonuma),后来负责制作了很多部塞尔达传说 (The Legend of Zelda)系列的作品,包括最近的,游戏卖的比主机还多的,旷野之息。

最早的塞尔达传说是来自大名鼎鼎的宫本茂,可能他更有名的游戏作品是超级马里 奥 (Super Mario),以前国内都叫超级玛丽。书里也重点写了他的故事。

书里很大一部分篇幅是在讲 Nintendo of America 的故事,山内溥把女婿荒川实(Minoru Arakawa)忽悠来帮他去开拓美国市场。荒川实这边就是一个艰苦创业的故事,挺精彩的。

另外有一条很重要的线是俄罗斯方块 (Teris) 的专利权的故事,俄罗斯方块这个游戏很厉害,当时出来的时候基本上大家一玩就上瘾。但是游戏的专利权归属真是错综复杂,各个巨头派人远赴苦寒之地明争暗斗要拿到专利授权。

书中也很详细的讲到了同期的其他游戏公司的故事,比如EA、世嘉 (Sega) 、Sony。

成书的时候N64即将诞生,作者也对此表示非常期待。任天堂在8位机的时代称霸一时,16位机的时候错失良机,希望一举跳到64位来个降维打击。

虽然之后N64并没有如任天堂希望的一样重回巅峰,算是输给了同世代的PS1。其中也有很多是任天堂自己的决策问题。但是N64上还是出了很多经典的游戏,比如塞尔达传说时之笛和超级马里奥64。现在我还留着3DS掌机的原因就是因为时之笛,不知道会不会有Switch的重制版本。

想来除了专业书以外,这本应该是我看过的最长的英文版的书了。之前看《The Order of Time》的时候,到了最后几章看得昏昏欲睡,放下之后就拿不起来了。想来这本书前后也看了好长时间,如果不是因为讲的是老任的辉煌历史,估计也是看不下来的。

A PyTorch GPU Memory Leak Example

I ran into this GPU memory leak issue when building a PyTorch training pipeline. After spending quite some time, I finally figured out this minimal reproducible example.

import torch

class AverageMeter(object):
    """
    Keeps track of most recent, average, sum, and count of a metric.
    """

    def __init__(self):
        self.reset()

    def reset(self):
        self.val = 0
        self.avg = 0
        self.sum = 0
        self.count = 0

    def update(self, val, n=1):
        self.val = val
        self.sum += val * n
        self.count += n
        self.avg = self.sum / self.count

device: torch.device = torch.device("cuda:0")
model = torch.hub.load('pytorch/vision:v0.9.0', 'resnet18', pretrained=False)
model.to(device)
model.train()

data = torch.zeros(size=[1,3,128,128],device=device,dtype=torch.float)
loss_avg = AverageMeter()
for i in range(1000):
    outputs = model(data)
    loss = outputs.mean()
    loss_avg.update(loss)
    if i % 10 == 0:
        print ('Loss, current batch {:.3f}, moving average {:.3f}'.format(loss_avg.val, loss_avg.avg))
        print ('GPU Memory Allocated {} MB'.format(torch.cuda.memory_allocated(device=device)/1024./1024.))

Kicking off the training, it shows constantly increasing allocated GPU memory.

Using cache found in /home/haoxiangli/.cache/torch/hub/pytorch_vision_v0.9.0
Loss, current batch -0.001, moving average -0.001
GPU Memory Allocated 51.64306640625 MB
Loss, current batch -0.001, moving average -0.001
GPU Memory Allocated 119.24072265625 MB
Loss, current batch -0.001, moving average -0.001
GPU Memory Allocated 186.83837890625 MB
Loss, current batch -0.001, moving average -0.001
GPU Memory Allocated 254.43603515625 MB
Loss, current batch -0.001, moving average -0.001
GPU Memory Allocated 322.03369140625 MB
Loss, current batch -0.001, moving average -0.001
GPU Memory Allocated 389.63134765625 MB
Loss, current batch -0.001, moving average -0.001

This “AverageMeter” has been used in many popular repositories (e.g., https://github.com/facebookresearch/moco). It’s by-design tracking the average of a given value and can be used to track training speed, loss value, and etc.

class AverageMeter(object):
    """
    Keeps track of most recent, average, sum, and count of a metric.
    """

    def __init__(self):
        self.reset()

    def reset(self):
        self.val = 0
        self.avg = 0
        self.sum = 0
        self.count = 0

    def update(self, val, n=1):
        self.val = val
        self.sum += val * n
        self.count += n
        self.avg = self.sum / self.count

It comes with some existing training pipeline code but once I got this into my codebase, I started to use it elsewhere.

The implementation is straightforward and bug-free but it turns out there is something tricky here.

Following is a modified version without the GPU memory leak problem:

import torch

class AverageMeter(object):
    """
    Keeps track of most recent, average, sum, and count of a metric.
    """

    def __init__(self):
        self.reset()

    def reset(self):
        self.val = 0
        self.avg = 0
        self.sum = 0
        self.count = 0

    def update(self, val, n=1):
        self.val = val
        self.sum += val * n
        self.count += n
        self.avg = self.sum / self.count

device: torch.device = torch.device("cuda:0")
model = torch.hub.load('pytorch/vision:v0.9.0', 'resnet18', pretrained=False)
model.to(device)
model.train()

data = torch.zeros(size=[1,3,128,128],device=device,dtype=torch.float)
loss_avg = AverageMeter()
for i in range(1000):
    outputs = model(data)
    loss = outputs.mean()
    loss_avg.update(loss.item()) # <-----
    if i % 10 == 0:
        print ('Loss, current batch {:.3f}, moving average {:.3f}'.format(loss_avg.val, loss_avg.avg))
        print ('GPU Memory Allocated {} MB'.format(torch.cuda.memory_allocated(device=device)/1024./1024.))

The annotated line is the little nuance. When something part of the computation graph is tracked with the “AverageMeter”, somehow PyTorch stops releasing related part of GPU memory. The fix is to cast it into a plain value beforehand.

This paradigm may show in the codebase in other forms. Once there is some utility classes being implemented, it is quite easy to accidentally use it over trainable PyTorch tensors. I do feel this is a bug of PyTorch (at least <=1.8.0) though.

网: 阿加西自传

https://book.douban.com/subject/30164685/

我不懂网球,读这本书之前我对这个人一无所知。单纯看到评分不错,就找来读了。读过感觉是很诚恳的自传,没有炫耀,没有鸡汤。从自己儿时被父亲逼着训练开始,能够从作者的文字中感受到他内心的自我矛盾。阿加西说他讨厌网球,他讨厌训练,他很叛逆,不论是在学校还是在网球训练基地。但是他也很想赢。整本书尤其前半段,阿加西的生活中都有种很纠结的情绪在里面。到了后半部分,格拉芙走进他生活之后,阿加西字里行间都是对格拉芙的爱慕和欣赏,格拉芙让他获得了内心的平静。

阿加西应该是一个内心世界极为丰富的人,作为一名顶级网球运动员,通过他对比赛的描述,感觉到在赛场上,他不是在和对手比技术技巧,而是比谁更能控制自己的内心。有几次他心态失控的时候,作为种子选手却第一场就输掉出局。每次叙述比赛的时候不是在说谁的球技更高超,而是在强调谁更加专注,谁更自省,谁更想赢。

书以第一人称的角度写的,但不是回忆的视角,是以当时的阿加西的口吻描述的。而且叙述上很真实的反映那个时期他的想法和情绪,很有画面感。确实是很好的一本自传。