<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://blog.shengbin.me/feed.xml" rel="self" type="application/atom+xml" /><link href="https://blog.shengbin.me/" rel="alternate" type="text/html" /><updated>2026-05-07T08:14:23+08:00</updated><id>https://blog.shengbin.me/feed.xml</id><title type="html">胜彬的博客</title><subtitle>Just write.</subtitle><entry><title type="html">万智牌竞技场</title><link href="https://blog.shengbin.me/posts/mtg-arena" rel="alternate" type="text/html" title="万智牌竞技场" /><published>2026-04-30T08:00:00+08:00</published><updated>2026-04-30T08:00:00+08:00</updated><id>https://blog.shengbin.me/posts/mtg-arena</id><content type="html" xml:base="https://blog.shengbin.me/posts/mtg-arena"><![CDATA[<p>万智牌（Magic The Gathering， MTG）是一种集换式卡牌，它的线上版本称为万智牌竞技场（MTG Arena）。</p>

<!--more-->

<p>我从去年五月份开始入坑万智牌。虽然也买过一些实体卡牌，方便和孩子在家玩，但我主要还是使用万智牌竞技场在线玩。
从定位上看，万智牌的线上版本是为辅助其实体版本而存在的。这点不同于我之前喜欢玩的三国杀。三国杀恐怕已经变得以线上为主，线下基本没什么存在感了吧。</p>

<h2 id="游戏模式">游戏模式</h2>

<p>万智牌竞技场中的游戏模式也和实体一样，分为构筑（Construct）和限制（Limit）两大类。
构筑模式允许玩家从自己拥有的整个牌库中选取卡牌来构筑牌组，前期需要收集卡牌来充实牌库，有了好的牌组后就可以无限次免费游玩了。
而限制模式则不同，每次都要花费购买新的卡牌包，并局限在新拆的包中来选取牌组（这也是“限制”名称的由来）。
两种模式都有需要付入场费的活动场，参与后根据输赢情况得到或多或少的奖励。</p>

<p>此外，玩家还可以选择是否参与排名（Ranked），也就是很多游戏都会有的天梯机制。
如果参与，则每次游戏的输赢都会最终决定其每个月的等级和名次。
不参与排名的话，就没有输赢的压力了（尤其是免费游玩时），不过似乎也就没什么动力了。</p>

<h2 id="竞技之路">竞技之路</h2>

<p>玩家一旦参与排名，便算是走上了竞技之路。</p>

<p>线上竞技的最终目标是在竞技场锦标赛（Arena Championship）中胜出。
这个赛事一年有3次，每隔4个月的周末两天举办。
竞技场锦标赛的参赛者主要通过每月一次的周末预选赛（Qualifier Weekend）来选拔。
而要参加周末预选赛，也需要在平时的线上游玩中提升排名、证明自己的实力。</p>

<p>每个月末所有玩家计算排名，前250名可以参加下个月的周末预选赛。
其他玩家如果想要参加，就需要先进行一次资格入围赛（Qualifier Play-In）。
资格入围赛在周末预选赛之前一周举行，人人都可以付费或是通过消耗20点入围点数（Play-In Points）来参加。</p>

<p>赚取入围点数有两个途径：一是进入月末排名的251至1200名，将直接获得20点；二是在平时的活动中胜利通关，每次可获得1点。</p>

<p>总之，参加入围赛可以靠钱也可以靠技术，但接下来参加预选赛和锦标赛则只能靠技术。</p>

<h2 id="从线上到线下">从线上到线下</h2>

<p>竞技场锦标赛的优胜者除了获得奖金，前2名将直接受邀参加万智牌领域的最高规格赛事：世界锦标赛（Magic World Championship）。
前16名也将受邀参加职业巡回赛（Pro Tour），从中争取世界锦标赛的名额。这些线下赛事等有机会再另写一篇文章细说。</p>]]></content><author><name></name></author><category term="生活" /><category term="游戏" /><summary type="html"><![CDATA[万智牌（Magic The Gathering， MTG）是一种集换式卡牌，它的线上版本称为万智牌竞技场（MTG Arena）。]]></summary></entry><entry><title type="html">虚惊一场</title><link href="https://blog.shengbin.me/posts/false-alarm" rel="alternate" type="text/html" title="虚惊一场" /><published>2026-04-18T20:00:00+08:00</published><updated>2026-04-18T20:00:00+08:00</updated><id>https://blog.shengbin.me/posts/false-alarm</id><content type="html" xml:base="https://blog.shengbin.me/posts/false-alarm"><![CDATA[<p>不知道是否因为我刚写了上一篇文章<a href="/posts/sudden-cardiogenic-death">《心源性猝死及其救治》</a>的缘故，最近经历了心脏相关的虚惊一场。</p>

<!--more-->

<p>事情开始于大概两周前。当时，我带孩子去游戏厅玩，回来的路上感觉到有点心跳不稳和大脑眩晕。
因为不久前才了解过心源性猝死，所以我十分担心，立即就去医院胸痛中心做了急诊检查。
检查的目的是排除心肌梗死。结果指标都正常，我就回家了。</p>

<p>谁料接下来这几天症状越来越多，越来越吓人。我记录的有：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>左胸口刺痛，隔几分钟一次，持续一两小时。
睡觉前躺下的时候出汗、眩晕；正胸口堵得慌，像压了块石头似的。
走路快一点或上下楼梯时，左心口闷痛；有时感觉胃痛。
早上进电梯时有点晕；站立一会儿左肩刺痛，隔一分钟一次，持续多次。
多次左胸口闷痛、左下腹痛、左后背痛、左上臂痛……
</code></pre></div></div>

<p>网上一搜，这些都符合心肌梗死或心肌缺血的症状。于是我再次约了门诊去看医生。
这次医生听了我的描述，看了心电图没啥异常，就问我最近是不是焦虑。
我当时挺纳闷，最近没什么焦虑的事情啊。
医生也不好明确下结论，只是比较轻描淡写的说道“应该不是心脏的问题”。
我提前做过了调研，便商量道：“我看网上说心梗的话做心脏彩超也查不出啥来，心电图也是发作后才能看出来，是不是得做动态心电图、冠脉CTA或冠脉造影啊。”
医生说，“你这种情况一般还没到需要做那些的时候，你要实在担心的话我给你开个运动心电图单子吧，你去看啥时候能约上。”
最后医生还补充了一句，“别老去网上看”，“去神经内科看看”。</p>

<p>运动负荷心电图约到了一周后。等待期间，我症状还在，而且继续上网到处查资料。
在查资料时，我发现原来有一种病叫做“<a href="https://baike.baidu.com/item/%E5%BF%83%E8%84%8F%E7%A5%9E%E7%BB%8F%E5%AE%98%E8%83%BD%E7%97%87">心脏神经官能症</a>”。
它是说心脏本身并没有器质性病变，纯心理作用导致的心血管相关的不适感；而且患者越是担心忧虑，症状反而越持续不消失。
再联想到前面医生说的话，我有点明白了。
她估计已经猜到我是得了这种神经症，但没法把话说绝对，只能通过进一步检查来排除心脏问题。</p>

<p>后来的运动负荷心电图没发现任何异常，而且为了更加放心，运动完我又立即抽血查了肌钙蛋白等心肌损伤的指标，最终也都正常。
这下基本可以排除心梗了。确实是虚惊一场。</p>

<p>经历了这一事件，我应该会更加重视身体健康了。
我之所以心理负担这么重，除了受心源性猝死的新闻影响外，还有个因素是近几年体检中血脂相关指标一直偏高。
虽然给医生看的时候她说你这高了一点没事，我还是不免为之焦虑（甚至得了医学意义上的焦虑症）。
此后，我将开始控制饮食，争取下次体检血脂正常。</p>]]></content><author><name></name></author><category term="生活" /><category term="知识" /><category term="记录" /><category term="健康" /><summary type="html"><![CDATA[不知道是否因为我刚写了上一篇文章《心源性猝死及其救治》的缘故，最近经历了心脏相关的虚惊一场。]]></summary></entry><entry><title type="html">心源性猝死及其急救</title><link href="https://blog.shengbin.me/posts/sudden-cardiogenic-death" rel="alternate" type="text/html" title="心源性猝死及其急救" /><published>2026-03-25T08:00:00+08:00</published><updated>2026-03-25T08:00:00+08:00</updated><id>https://blog.shengbin.me/posts/sudden-cardiogenic-death</id><content type="html" xml:base="https://blog.shengbin.me/posts/sudden-cardiogenic-death"><![CDATA[<p>猝死就是突然的死亡。医学上将猝死分为了两类：心源性和非心源性，其中心源性猝死占比80%。
可见心源性猝死的重要性。</p>

<!--more-->

<p>顾名思义，心源性猝死就是由来源于心脏的因素导致的突然死亡。
绝大部分情况下，这一致命因素是心室颤动，简称室颤。</p>

<p>室颤，即心室无规律地颤动，丧失了正常情况下有序跳动的泵血功能。
这种情况也称作心搏骤停。一旦心跳停了，最严重的后果就是大脑缺血，造成不可逆的损伤，最终导致死亡。
发生心搏骤停后的4分钟是抢救的关键期。
这期间实施心肺复苏术（CPR）并使用自动体外除颤仪（AED）可大大提高存活率。</p>

<p>CPR主要操作包括两方面：一是胸外按压来帮助泵血，二是人工呼吸来帮助供氧。
胸外按压约每秒两次，按压起伏要达到5厘米；每按30次做两次人工呼吸。
人工呼吸时捏住鼻子吹气1秒使其胸廓隆起，然后松开鼻子让其自然呼气。</p>

<p>如果说CPR是临时替代心脏的功能，那么AED的作用则是使心脏消除颤动、恢复正常功能。
AED一般都附带使用说明和语音提示，遵照使用即可。
需要注意的是，AED没到位之前，要先进行CPR；AED电击后，如果患者未恢复呼吸和心跳，则继续进行CPR，两分钟后AED会再次分析心律并提示操作。</p>

<p>现场的急救如果保住了大脑等重要器官，送医之后存活的概率就大大增加，也就防止了猝死。</p>

<p>心脏骤停的危急程度高于另一个容易与之混淆的概念：心肌梗死，简称心梗。
心梗是心血管的梗阻导致了心脏局部肌肉缺血坏死，虽然也影响心脏功能，但一般不会那么快危及生命。
急性心梗发作时会有持续30分钟左右的心前区疼痛和憋闷感，可舌下含服硝酸甘油缓解，然后就医。
和心梗类似的脑梗，是脑部组织因脑血管梗阻而坏死，症状有眩晕、瘫痪等。
心脑梗死的根源在于血脂异常、动脉硬化等一系列血管疾病，虽然也应重视，但和猝死的关系就另说了。</p>

<p>最后，要避免心源性猝死的发生，应尽量减少其诱发因素，例如过度劳累、剧烈运动、情绪异常等。
如果出现了先兆，如胸痛胸闷、心慌心悸、晕厥、疲乏等，更要警惕了！</p>

<p>爱情诚可贵，自由价更高，若为生命故，二者皆可抛。人到中年，愈加惜命。</p>]]></content><author><name></name></author><category term="生活" /><category term="知识" /><summary type="html"><![CDATA[猝死就是突然的死亡。医学上将猝死分为了两类：心源性和非心源性，其中心源性猝死占比80%。 可见心源性猝死的重要性。]]></summary></entry><entry><title type="html">Windows热点自动开启</title><link href="https://blog.shengbin.me/posts/auto-start-windows-hotspot" rel="alternate" type="text/html" title="Windows热点自动开启" /><published>2026-02-09T08:00:00+08:00</published><updated>2026-02-09T08:00:00+08:00</updated><id>https://blog.shengbin.me/posts/auto-start-windows-hotspot</id><content type="html" xml:base="https://blog.shengbin.me/posts/auto-start-windows-hotspot"><![CDATA[<p>我在自己的Windows电脑上设置了热点，但它默认情况下不会随着电脑启动而自动开启，而我又容易忘记手动操作。
为解决此问题，我增加了一个脚本来开启热点，并让该脚本开机自动执行。</p>

<!--more-->

<p>脚本内容如下：</p>

<div class="language-ps highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">;@Findstr</span> <span class="nf">-bv</span> <span class="nf">;@F</span> <span class="nf">"</span><span class="c1">%~f0" | powershell -command - &amp; goto:eof</span>

<span class="nf">Add-Type</span> <span class="nf">-AssemblyName</span> <span class="nf">System.Runtime.WindowsRuntime</span>

<span class="nf">$connectionProfile</span> <span class="nf">=</span> <span class="p">[</span><span class="nf">Windows.Networking.Connectivity.NetworkInformation,Windows.Networking.Connectivity,ContentType=WindowsRuntime</span><span class="p">]</span><span class="nf">::GetInternetConnectionProfile</span><span class="s">()</span>
<span class="nf">$tetheringManager</span> <span class="nf">=</span> <span class="p">[</span><span class="nf">Windows.Networking.NetworkOperators.NetworkOperatorTetheringManager,Windows.Networking.NetworkOperators,ContentType=WindowsRuntime</span><span class="p">]</span><span class="nf">::CreateFromConnectionProfile</span><span class="s">($connectionProfile)</span> 

<span class="nf">Function</span> <span class="nf">EnableHotspot</span> <span class="p">{</span>
    <span class="kr">if</span> <span class="s">($tetheringManager.TetheringOperationalState -eq 1)</span> <span class="p">{</span>
        <span class="nf">"Hotspot</span> <span class="nf">is</span> <span class="nf">already</span> <span class="nf">On!"</span>
    <span class="p">}</span> <span class="nf">else</span> <span class="p">{</span>
        <span class="nf">"Hotspot</span> <span class="nf">is</span> <span class="nf">off!</span> <span class="nf">Turning</span> <span class="nf">it</span> <span class="nf">on"</span>
        <span class="nf">$tetheringManager.StartTetheringAsync</span><span class="s">()</span> <span class="nf">|</span> <span class="nf">Out-Null</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="nf">Function</span> <span class="nf">DisableHotspot</span> <span class="p">{</span>
    <span class="kr">if</span> <span class="s">($tetheringManager.TetheringOperationalState -eq 0)</span> <span class="p">{</span>
        <span class="nf">"Hotspot</span> <span class="nf">is</span> <span class="nf">already</span> <span class="nf">Off!"</span>
    <span class="p">}</span> <span class="nf">else</span> <span class="p">{</span>
        <span class="nf">"Hotspot</span> <span class="nf">is</span> <span class="nf">on!</span> <span class="nf">Turning</span> <span class="nf">it</span> <span class="nf">off"</span>
        <span class="nf">$tetheringManager.StopTetheringAsync</span><span class="s">()</span> <span class="nf">|</span> <span class="nf">Out-Null</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="nf">EnableHotspot</span>
</code></pre></div></div>

<p>这里面的大部分内容是Windows PowerShell的代码，但它其实是作为批处理文件（.bat）来保存和运行的。
其中第一行的意思是将该文件之后的字符串都作为参数传给<code class="language-plaintext highlighter-rouge">powershell -command</code>命令。
该命令即调用系统的powershell解释器来执行传给它的脚本代码。
之所以这样做，是因为Windows不支持开机直接运行PowerShell的脚本文件，或许觉得其过于强大（Power），容易有安全隐患吧。
批处理文件不受这一限制，但它又不便于实现热点的开启，所以最终做成了上面这种PowerShell代码嵌入到批处理文件中的形式。</p>

<p>为使上述批处理文件开机自动运行，将其放在<code class="language-plaintext highlighter-rouge">C:\Users\Shengbin\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup</code>文件夹下即可。
此外还能通过添加任务计划程序或者修改注册表来实现，不过感觉没那么便捷了。</p>]]></content><author><name></name></author><category term="技术" /><category term="Windows" /><category term="Wi-Fi" /><summary type="html"><![CDATA[我在自己的Windows电脑上设置了热点，但它默认情况下不会随着电脑启动而自动开启，而我又容易忘记手动操作。 为解决此问题，我增加了一个脚本来开启热点，并让该脚本开机自动执行。]]></summary></entry><entry><title type="html">今年读的书</title><link href="https://blog.shengbin.me/posts/the-books-i-read-this-year" rel="alternate" type="text/html" title="今年读的书" /><published>2025-12-31T08:00:00+08:00</published><updated>2025-12-31T08:00:00+08:00</updated><id>https://blog.shengbin.me/posts/the-books-i-read-this-year</id><content type="html" xml:base="https://blog.shengbin.me/posts/the-books-i-read-this-year"><![CDATA[<p>2025年要结束了，并没有太多感慨要发。不过回顾了今年读的书，发现还是比较有特色的。
如果用一个关键词来概括的话，那就是：经典。</p>

<!--more-->

<p>下面是我今年<a href="https://book.douban.com/people/msb91/collect">读书</a>的汇总图。</p>

<p><img src="/images/2025-12-31-douban-books.jpg" alt="" /></p>

<p>图中显示读过14本，不过总册数应该有20了，因为其中的《银河帝国》，副标题“基地七部曲”，其实包括了7册。</p>

<p>今年读的“基地”系列和“太空漫游”系列，都是科幻小说的经典名著。
我之前看过改编的影视，印象就很深，尤其是《太空漫游2001》电影里那个猴子扔骨头变成飞船的场景。
几年前，我就收集了这两个系列的电子书，不过一直没能坚持读下去。
今年总算清掉了这两大存货。基地七部曲是买的纸质书，一口气看完；太空漫游系列则是读的电子版。</p>

<p>在找存货的时候，我发现自己的电子书文件夹里还有另一个未读的经典：《悲惨世界》。
这部小说的剧情我自以为很熟悉，因为我曾把它改编的歌剧和电影各个版本几乎都刷了个遍。
但遗憾的是之前一直没读原著。今年读完之后，确实酣畅淋漓，大为满足。
我特意再去看了一遍电影，才发现自己印象中震撼无比的电影与原著比起来，也只能说相形见绌了。
雨果这位文学大师，果然名不虚传。</p>

<p>另一部今年开读的经典是《计算机程序设计艺术》（The Art of Computer Programming，TAOCP）。
该书分为好几卷，我刚读完第一卷。作者实力不凡，据说仍在写作后面几卷。
我是慕名而读的，感觉这部书除了太难，倒也的确是本好书。</p>

<p>有时候对某些书多少会有点情结吧，不读不快。今年着实爽快了几次。</p>]]></content><author><name></name></author><category term="生活" /><category term="总结" /><category term="记录" /><summary type="html"><![CDATA[2025年要结束了，并没有太多感慨要发。不过回顾了今年读的书，发现还是比较有特色的。 如果用一个关键词来概括的话，那就是：经典。]]></summary></entry><entry><title type="html">我们的医保情况</title><link href="https://blog.shengbin.me/posts/our-medical-insurance" rel="alternate" type="text/html" title="我们的医保情况" /><published>2025-12-12T08:00:00+08:00</published><updated>2025-12-12T08:00:00+08:00</updated><id>https://blog.shengbin.me/posts/our-medical-insurance</id><content type="html" xml:base="https://blog.shengbin.me/posts/our-medical-insurance"><![CDATA[<p>最近孩子学校通知家长为明年的医保缴费，趁此机会我详细了解了下我们的医保情况。</p>

<!--more-->

<h2 id="费用和待遇">费用和待遇</h2>

<p>我们所在地北京，和其他城市应该一样，医保按人群分为两类：职工医疗保险和居民医疗保险。</p>

<h3 id="职工医疗保险">职工医疗保险</h3>

<p>对于职工医疗保险，每月需要缴纳的费用大概是工资的8%，由个人和所在单位分担。
我之前写的一篇文章<a href="/posts/social-insurance-and-housing-fund">《社保与公积金》</a>，里面提到过相关信息。</p>

<p>职工医疗保险的待遇如下图所示。这里只包含门急诊，另有住院的，因我们没涉及过，暂时忽略。</p>

<p><img src="/images/2025-12-12-employee-benefits.png" title="职工医疗保险的待遇（网图侵删）" width="500px" /></p>

<p>图中的起付线是指一年累计的就医花费超过这个值之后的部分才能由医保报销。
以在职职工为例，如果当年看病花的钱还没有超过1800元，是都要自费的。
不过挂号费似乎不受此限制，每次都可以报销一大部分。</p>

<h3 id="居民医疗保险">居民医疗保险</h3>

<p>居民包括老人、小孩、失业人员、农民等没有就职单位的人群。
居民医保费用一年仅需几百元，较职工的每月几百元甚至几千元要低不少。
我刚给两个孩子交的2026年费用，每人435元。</p>

<p>相比职工，居民医保的待遇也大不一样，起付线和报销比例要低很多。具体可参见下图。</p>

<p><img src="/images/2025-12-12-resident-benefits.png" title="居民医疗保险的待遇（网图侵删）" width="600px" /></p>

<p>这个差异的初衷大概是考虑居民群体收入低、小病多、买药频繁等特点，让他们不用花太多钱就很容易得到报销。
按医院级别区分，则明显是鼓励大家多去小医院，没事别都往三甲挤……</p>

<h2 id="家庭共济和代办">家庭共济和代办</h2>

<p>从前几年开始，职工每月缴纳的医保个人部分无法再提取出来了，只能放在帐户里消费。
与之配套出台的政策是家庭共济制度，也就是说，家庭成员可以花我医保帐户里的钱了。
此后，我家两个孩子的（包括我自己的）就医花费就全由我的医保缴费（个人部分）来覆盖了。</p>

<p>不仅他们看病时直接由我代扣费用，甚至他们每年缴纳的医保费用也是如此。
前面说到，我刚给孩子交的每人435元钱，其实是自动从我医保帐户里扣除的，都没经我操作！</p>

<p>当然，这样便捷扣费的前提是我将俩娃绑定为了家庭共济成员。
此外，我发现医保系统还支持将我绑定为他们的代办人，在线代其办理查询、变更等各项事务。
于是，我登录系统查询了自己和两个孩子过去几年的医疗消费情况，下面做个回顾。</p>

<h2 id="历年回顾">历年回顾</h2>

<p>从北京市医保局的医保服务网站（<a href="https://ybfw.ybj.beijing.gov.cn/">https://ybfw.ybj.beijing.gov.cn/</a>，网址是拼音首字母，还算美观）可以很方便查询本人和代查他人的数据。
查询我家二娃今年的医疗消费信息结果截图如下：</p>

<p><img src="/images/2025-12-12-zhengyan-2025-cost.png" alt="" /></p>

<p>可以看出，这小家伙2025年上半年和下半年各去了一趟海淀医院，共花了6笔费用，总计841.96元，其中医保给报了93.37元。
这和上面展示的居民医保门诊（二级和三级医院）起付线550元，报销比例50%大致是相符的。</p>

<p>由于今年他的医保报销（仅93元左右）少于缴费（400元左右），因此从结果来看是“亏钱”的，算是为医保基金做了贡献。
2024年，其医疗费用总额2256元，医保支付总额717元，算是“赚到”了一点。
2023年，花费693元，报销40元，更是亏了不少……
不过呢，这方面的亏赚不能只看钱，如果我们认为健康更重要，那么赚即是亏，亏即是赚！</p>

<p>下表列出我家大娃和我自己近几年的医疗费用（括号外是总额，括号内是医保报销的部分）：</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>2021年</th>
      <th>2022年</th>
      <th>2023年</th>
      <th>2024年</th>
      <th>2025年</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>我家大娃</td>
      <td>2286（845）</td>
      <td>5475（2197）</td>
      <td>8183（3361）</td>
      <td>4522（2016）</td>
      <td>1631（528）</td>
    </tr>
    <tr>
      <td>我自己</td>
      <td>327（80）</td>
      <td>605（80）</td>
      <td>588（40）</td>
      <td>2246（238）</td>
      <td>5004（2255）</td>
    </tr>
  </tbody>
</table>

<p>可以看到，老大过去这几年看病花了不少钱。
其实都是感冒、鼻炎这类我觉得不去医院也行但他妈妈非要去的病症，今年开始没那么多了。
今年我自己的花费突增，是因为做了一个牙齿根管治疗。</p>

<p>临近年底了，对医疗花费情况做个年度回顾，倒也不失为一种颇合时宜的跟风之作。</p>]]></content><author><name></name></author><category term="生活" /><category term="知识" /><category term="记录" /><category term="娃" /><summary type="html"><![CDATA[最近孩子学校通知家长为明年的医保缴费，趁此机会我详细了解了下我们的医保情况。]]></summary></entry><entry><title type="html">从Windows克隆Git仓库</title><link href="https://blog.shengbin.me/posts/git-clone-from-windows" rel="alternate" type="text/html" title="从Windows克隆Git仓库" /><published>2025-08-26T08:00:00+08:00</published><updated>2025-08-26T08:00:00+08:00</updated><id>https://blog.shengbin.me/posts/git-clone-from-windows</id><content type="html" xml:base="https://blog.shengbin.me/posts/git-clone-from-windows"><![CDATA[<p>Windows不如Linux更适合开发者。当我试图从Windows上克隆一个Git仓库时再次深刻体会到了这一点。</p>

<!--more-->

<p>如果Git仓库是在Linux系统上，例如位于<code class="language-plaintext highlighter-rouge">/projects-path/my-repo</code>，我们可以直接通过加上远端机器地址前缀（host）来将它克隆下来：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone host:/projects-path/my-repo
</code></pre></div></div>

<p>和从本地目录克隆一样简单。</p>

<p>不过我面临的情况下，仓库位于另一台Windows机器上（<code class="language-plaintext highlighter-rouge">Z:\projects-path\my-repo</code>），上述命令就没那么容易成功了。</p>

<p>首先，Windows默认是没有开启SSH服务的，需要操作一番。这包括安装这一可选功能、启动服务、设置为自启动。
好在启动SSH服务时Windows系统会自动增加防火墙规则来允许22端口的通信。</p>

<p>如果你像我一样，曾怀疑连接到Windows机器的网络有问题，想通过ping命令测试一下，那就容易被误导。
默认情况下是ping不通的，并非因为网络不通，而是因为Windows防火墙禁用了ping命令所依赖的ICMP协议。
这在Linux上理所当然能用的功能在Windows上还要手动配置一下。</p>

<p>接下来，Git能通过SSH访问Windows了，但执行<code class="language-plaintext highlighter-rouge">git clone host:/Z:/projects-path/my-repo</code>总是报如下错误：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>fatal: ''/Z:/projects-path/my-repo'' does not appear to be a git repository
</code></pre></div></div>

<p>看到这一报错第一感觉是路径形式没写对。
可是试了把<code class="language-plaintext highlighter-rouge">/Z:/projects-path/my-repo</code>换为<code class="language-plaintext highlighter-rouge">Z:/projects-path/my-repo</code>、<code class="language-plaintext highlighter-rouge">/Z/projects-path/my-repo</code>等多种形式都报错，斜杠换成反斜杠也不行。</p>

<p>查到网上<a href="https://superuser.com/a/1841390/3112705">一个帖子</a>才总算弄清楚了，原来是Windows的SSH服务默认使用的终端Cmd.exe存在路径兼容性问题。
这时也才注意到，上面报错中有两个引号，Cmd.exe把<code class="language-plaintext highlighter-rouge">'/Z:/projects-path/my-repo'</code>（包括引号）当作路径去使用了。
按照该帖子的方法替换Git内部工具（git-upload-pack）果然就可以成功克隆了。</p>

<p>在替换Git内部工具的过程中，需要修改<code class="language-plaintext highlighter-rouge">PATH</code>环境变量。为使修改生效，SSH服务也要重启。此外，对于SSH服务使用的环境变量，不能用<code class="language-plaintext highlighter-rouge">%USERPROFILE%</code>代替用户目录（我干脆直接写了绝对路径），因为会被替换为另一个配置项……这都是操作过程中踩出的坑啊。</p>

<p>最后虽然达到了目的，但我总觉得魔改Git内部工具的方法有点不优雅。
既然是Cmd.exe的兼容性导致的问题，那是不是将SSH服务的终端换为PowerShell也能解决呢？
这个方法经验证也是可行的。
不过要替换SSH服务所用的终端呢，就只能通过Windows代表性手段——修改注册表——来实现了！
具体参见我在<a href="https://superuser.com/a/1841390/3112705">那个帖子</a>下面评论里做的补充。</p>]]></content><author><name></name></author><category term="技术" /><category term="Windows" /><category term="Git" /><summary type="html"><![CDATA[Windows不如Linux更适合开发者。当我试图从Windows上克隆一个Git仓库时再次深刻体会到了这一点。]]></summary></entry><entry><title type="html">Ubuntu下U盘弹出很慢的问题</title><link href="https://blog.shengbin.me/posts/ubuntu-usb-disk-eject-slow" rel="alternate" type="text/html" title="Ubuntu下U盘弹出很慢的问题" /><published>2025-04-24T08:00:00+08:00</published><updated>2025-04-24T08:00:00+08:00</updated><id>https://blog.shengbin.me/posts/ubuntu-usb-disk-eject-slow</id><content type="html" xml:base="https://blog.shengbin.me/posts/ubuntu-usb-disk-eject-slow"><![CDATA[<p>最近在从Ubuntu电脑向U盘拷贝大文件时，遇到了一个问题。
文件管理器显示拷贝很快就完成了，但此时若要弹出U盘则会显示“正在将数据写入，不要拔掉设备”的通知。
如果忽略该通知再次点击弹出，就会显示“无法弹出，一项操作正在进行中”。
等一段时间之后，才会收到可以拔出设备的通知。
拷贝的文件越大，这一等待的时间就越长。</p>

<!--more-->

<h2 id="原因">原因</h2>

<p>这一现象看上去就令人怀疑是缓存导致的。通过查一些资料，确认了原因果然如此：Linux系统在拷贝文件时，会先将文件写入内存（作为写缓存），然后再从内存写入磁盘。写缓存（又称为脏内存）的大小可以通过下面的变量来配置为特定字节数或是占系统总内存的比例。</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">shengbin@Suma-W40P:~$</span><span class="w"> </span>sysctl vm.dirty_bytes
<span class="go">vm.dirty_bytes = 0
</span><span class="gp">shengbin@Suma-W40P:~$</span><span class="w"> </span>sysctl vm.dirty_ratio
<span class="go">vm.dirty_ratio = 20
</span></code></pre></div></div>

<p>可以看到，Ubuntu系统将其默认配置为了总内存的20%。由于我的总内存有128G，写缓存就有25.6G。
当向U盘拷贝低于该大小的文件时，全部数据先被快速拷贝到了缓存里，然后才慢慢写入U盘（从缓存写入目标磁盘的操作是自动进行的，但也可以通过<code class="language-plaintext highlighter-rouge">sync</code>命令手动发起）。
而无论是命令行的<code class="language-plaintext highlighter-rouge">cp</code>还是图形界面的文件管理器，都报告的是拷贝到缓存里的速度和进度。
之后慢慢写入的过程就导致了需要等一段时间才能弹出U盘的现象。
拷贝的文件越大，U盘写入越慢，需要等的时间就越长，甚至会让人以为是卡住了。用户体验确实较差！</p>

<h2 id="解决方法">解决方法</h2>

<p>要避免这一现象，我们可以完全禁用上述这种利用写缓存进行异步读写的机制，改为同步读写。不过这样会有两个负面影响，一是导致总拷贝耗时有所增加，二是过于频繁写入磁盘的话其使用寿命可能缩短。更好的方法是仍然采用缓存，但不要这么大。如果我们修改<code class="language-plaintext highlighter-rouge">vm.dirty_bytes</code>或<code class="language-plaintext highlighter-rouge">vm.dirty_ratio</code>的值，那所有磁盘写入都会受影响，所以最好的方法是只针对U盘来配置较小的写缓存。</p>

<p>好在，Linux从某个较新的内核开始，支持了这么做。只需在<code class="language-plaintext highlighter-rouge">/etc/udev/rules.d/</code>目录下新建一个规则文件<code class="language-plaintext highlighter-rouge">99-usb-sync.rules</code>，写进如下内容：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ACTION=="add", KERNEL=="sd[a-z]", SUBSYSTEM=="block", ENV{ID_USB_TYPE}=="disk", RUN+="/usr/bin/bash -c 'echo 1 &gt; /sys/block/%k/bdi/strict_limit; echo 16777216 &gt; /sys/block/%k/bdi/max_bytes'"
</code></pre></div></div>

<p>这一规则会在有U盘插入时，通过<code class="language-plaintext highlighter-rouge">/sys/block/%k/bdi/max_bytes</code>为其配置最大写缓存。</p>

<p>上述改动在重启后生效。不重启的情况下也可以通过执行如下命令使其生效：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo udevadm control --reload-rules &amp;&amp; sudo udevadm trigger
</code></pre></div></div>

<h2 id="结果">结果</h2>

<p>在进行上述改动前：</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">shengbin@Suma-W40P:~$</span><span class="w"> </span><span class="nb">time cp</span> ~/Downloads/docker-desktop-amd64.deb /media/shengbin/TPU301/ <span class="o">&amp;&amp;</span> <span class="nb">time sync</span>
<span class="go">
real    0m1.167s
user    0m0.001s
sys     0m1.165s

real    0m8.191s
user    0m0.000s
sys     0m0.004s
</span></code></pre></div></div>

<p>可以看到，<code class="language-plaintext highlighter-rouge">cp</code>命令1秒多就完成了，而之后从缓存实际写入磁盘的<code class="language-plaintext highlighter-rouge">sync</code>命令耗时却长达8秒多。</p>

<p>在进行上述改动后：</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">shengbin@Suma-W40P:~$</span><span class="w"> </span><span class="nb">time cp</span> ~/Downloads/docker-desktop-amd64.deb /media/shengbin/TPU301/ <span class="o">&amp;&amp;</span> <span class="nb">time sync</span>
<span class="go">
real    0m8.476s
user    0m0.003s
sys     0m0.878s

real    0m0.032s
user    0m0.001s
sys     0m0.003s
</span></code></pre></div></div>

<p>可见<code class="language-plaintext highlighter-rouge">cp</code>命令就真实反映了所有拷贝耗时。相应的，当图形界面的文件管理器告知说拷贝进度完成的时候，数据也确实已经全部写入了磁盘，此时弹出U盘就可以立即进行了。</p>

<p>其实，这一问题影响体验的地方也主要是图形界面用户。
<a href="https://forum.manjaro.org/t/strict-limit-of-write-cache-0s-sync-time-policy-for-usb-devices-by-default/166934/3">有网友提出</a>，文件管理器的开发者应该在其内部调用<code class="language-plaintext highlighter-rouge">sync</code>命令来确保缓存的数据也全部写入之后再报告拷贝完成。
我认同这一观点，这样确实符合了普通用户的预期，也不需要进行本文所说的修改了。</p>]]></content><author><name></name></author><category term="技术" /><category term="Linux" /><summary type="html"><![CDATA[最近在从Ubuntu电脑向U盘拷贝大文件时，遇到了一个问题。 文件管理器显示拷贝很快就完成了，但此时若要弹出U盘则会显示“正在将数据写入，不要拔掉设备”的通知。 如果忽略该通知再次点击弹出，就会显示“无法弹出，一项操作正在进行中”。 等一段时间之后，才会收到可以拔出设备的通知。 拷贝的文件越大，这一等待的时间就越长。]]></summary></entry><entry><title type="html">Segment Anything Model (SAM) 的接口参数</title><link href="https://blog.shengbin.me/posts/segment-anything-model-interface-params" rel="alternate" type="text/html" title="Segment Anything Model (SAM) 的接口参数" /><published>2025-04-08T20:00:00+08:00</published><updated>2025-04-08T20:00:00+08:00</updated><id>https://blog.shengbin.me/posts/segment-anything-model-interface-params</id><content type="html" xml:base="https://blog.shengbin.me/posts/segment-anything-model-interface-params"><![CDATA[<p>Segment Anything Model (SAM) 是一个分割模型。它的接口参数只有很简单的文档说明。
我在使用中弄清楚了其含义，本文做个解释。如无特殊说明，这里提到的SAM是指其当前最新版本<a href="https://ai.meta.com/sam2/">SAM 2</a>。</p>

<!--more-->

<p>一般来说，SAM接受用户指定的点或框来作为提示（prompts），然后推理给出想要的物体掩膜。
对于图片分割，直接使用<a href="https://github.com/facebookresearch/sam2/blob/2b90b9f5ceec907a1c18123530e92e794ad901a4/sam2/sam2_image_predictor.py#L237"><code class="language-plaintext highlighter-rouge">SAM2ImagePredictor.predict</code></a>即可。
该函数输入为提示（除点和框外，还接受掩膜作为提示），输出为目标物体的掩膜。
对于视频的分割和追踪，实现同样功能的是<code class="language-plaintext highlighter-rouge">SAM2VideoPredictor</code>的<a href="https://github.com/facebookresearch/sam2/blob/2b90b9f5ceec907a1c18123530e92e794ad901a4/sam2/sam2_video_predictor.py#L161"><code class="language-plaintext highlighter-rouge">add_new_points_or_box</code></a>和<a href="https://github.com/facebookresearch/sam2/blob/2b90b9f5ceec907a1c18123530e92e794ad901a4/sam2/sam2_video_predictor.py#L300"><code class="language-plaintext highlighter-rouge">add_new_mask</code></a>函数。
这两个函数对视频的某一帧添加提示、进行分割并返回结果，同时也为追踪提供了物体信息。
在对某些帧（通常是第一帧）添加过提示之后，就可以调用<a href="https://github.com/facebookresearch/sam2/blob/2b90b9f5ceec907a1c18123530e92e794ad901a4/sam2/sam2_video_predictor.py#L546"><code class="language-plaintext highlighter-rouge">SAM2VideoPredictor.add_new_points_or_box</code></a>来进行追踪，返回整个视频中各帧各物体的时空坐标（SMA 2称为masklet）。</p>

<p>上面这几个功能及其接口都容易理解。我想要解释的其实是另一个功能的接口，即为一张图片自动生成所有物体的掩膜。
这一功能通过下面的类<a href="https://github.com/facebookresearch/sam2/blob/2b90b9f5ceec907a1c18123530e92e794ad901a4/sam2/automatic_mask_generator.py#L36">SAM2AutomaticMaskGenerator</a>来实现：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">SAM2AutomaticMaskGenerator</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span>
        <span class="bp">self</span><span class="p">,</span>
        <span class="n">model</span><span class="p">:</span> <span class="n">SAM2Base</span><span class="p">,</span>
        <span class="n">points_per_side</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">int</span><span class="p">]</span> <span class="o">=</span> <span class="mi">32</span><span class="p">,</span>
        <span class="n">points_per_batch</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">64</span><span class="p">,</span>
        <span class="n">pred_iou_thresh</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">0.8</span><span class="p">,</span>
        <span class="n">stability_score_thresh</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">0.95</span><span class="p">,</span>
        <span class="n">stability_score_offset</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">1.0</span><span class="p">,</span>
        <span class="n">mask_threshold</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">0.0</span><span class="p">,</span>
        <span class="n">box_nms_thresh</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">0.7</span><span class="p">,</span>
        <span class="n">crop_n_layers</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span>
        <span class="n">crop_nms_thresh</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">0.7</span><span class="p">,</span>
        <span class="n">crop_overlap_ratio</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mi">512</span> <span class="o">/</span> <span class="mi">1500</span><span class="p">,</span>
        <span class="n">crop_n_points_downscale_factor</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">1</span><span class="p">,</span>
        <span class="n">point_grids</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="n">List</span><span class="p">[</span><span class="n">np</span><span class="p">.</span><span class="n">ndarray</span><span class="p">]]</span> <span class="o">=</span> <span class="bp">None</span><span class="p">,</span>
        <span class="n">min_mask_region_area</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span>
        <span class="n">output_mode</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s">"binary_mask"</span><span class="p">,</span>
        <span class="n">use_m2m</span><span class="p">:</span> <span class="nb">bool</span> <span class="o">=</span> <span class="bp">False</span><span class="p">,</span>
        <span class="n">multimask_output</span><span class="p">:</span> <span class="nb">bool</span> <span class="o">=</span> <span class="bp">True</span><span class="p">,</span>
        <span class="o">**</span><span class="n">kwargs</span><span class="p">,</span>
    <span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
        <span class="s">"""
        Using a SAM 2 model, generates masks for the entire image.
        Generates a grid of point prompts over the image, then filters
        low quality and duplicate masks. The default settings are chosen
        for SAM 2 with a HieraL backbone.

        Arguments:
          model (Sam): The SAM 2 model to use for mask prediction.
          points_per_side (int or None): The number of points to be sampled
            along one side of the image. The total number of points is
            points_per_side**2. If None, 'point_grids' must provide explicit
            point sampling.
          points_per_batch (int): Sets the number of points run simultaneously
            by the model. Higher numbers may be faster but use more GPU memory.
          pred_iou_thresh (float): A filtering threshold in [0,1], using the
            model's predicted mask quality.
          stability_score_thresh (float): A filtering threshold in [0,1], using
            the stability of the mask under changes to the cutoff used to binarize
            the model's mask predictions.
          stability_score_offset (float): The amount to shift the cutoff when
            calculated the stability score.
          mask_threshold (float): Threshold for binarizing the mask logits
          box_nms_thresh (float): The box IoU cutoff used by non-maximal
            suppression to filter duplicate masks.
          crop_n_layers (int): If &gt;0, mask prediction will be run again on
            crops of the image. Sets the number of layers to run, where each
            layer has 2**i_layer number of image crops.
          crop_nms_thresh (float): The box IoU cutoff used by non-maximal
            suppression to filter duplicate masks between different crops.
          crop_overlap_ratio (float): Sets the degree to which crops overlap.
            In the first crop layer, crops will overlap by this fraction of
            the image length. Later layers with more crops scale down this overlap.
          crop_n_points_downscale_factor (int): The number of points-per-side
            sampled in layer n is scaled down by crop_n_points_downscale_factor**n.
          point_grids (list(np.ndarray) or None): A list over explicit grids
            of points used for sampling, normalized to [0,1]. The nth grid in the
            list is used in the nth crop layer. Exclusive with points_per_side.
          min_mask_region_area (int): If &gt;0, postprocessing will be applied
            to remove disconnected regions and holes in masks with area smaller
            than min_mask_region_area. Requires opencv.
          output_mode (str): The form masks are returned in. Can be 'binary_mask',
            'uncompressed_rle', or 'coco_rle'. 'coco_rle' requires pycocotools.
            For large resolutions, 'binary_mask' may consume large amounts of
            memory.
          use_m2m (bool): Whether to add a one step refinement using previous mask predictions.
          multimask_output (bool): Whether to output multimask at each point of the grid.
        """</span>
    <span class="p">...</span>
    <span class="p">...</span>
</code></pre></div></div>

<p>使用这个类的主要参数就是在初始化时指定的这些，进行推理时反而简单调用<code class="language-plaintext highlighter-rouge">generate(self, image)</code>即可。
它的工作原理是自动在图片中选出一些点作为提示，然后进行分割；还支持从原始图片中截取出部分小图来多次处理；最后对所有分割结果进行过滤，筛出有价值的掩膜。</p>

<p>这些初始化参数中，有些（<code class="language-plaintext highlighter-rouge">points_per_side</code>、<code class="language-plaintext highlighter-rouge">point_grids</code>、<code class="language-plaintext highlighter-rouge">crop_n_points_downscale_factor</code>、<code class="language-plaintext highlighter-rouge">crop_n_layers</code>、<code class="language-plaintext highlighter-rouge">crop_overlap_ratio</code>）用于指定取哪些点作为提示（包括截取的小图中的点），另外大多用于指定对结果过滤筛选的条件，尤其是几个以<code class="language-plaintext highlighter-rouge">thresh</code>结尾的阈值。
其中<code class="language-plaintext highlighter-rouge">pred_iou_thresh</code>、<code class="language-plaintext highlighter-rouge">box_nms_thresh</code>, <code class="language-plaintext highlighter-rouge">crop_nms_thresh</code>的含义比较易懂，而<code class="language-plaintext highlighter-rouge">stability_score_thresh</code>则不太好理解。
我是通过阅读<a href="https://github.com/facebookresearch/sam2/blob/main/sam2/utils/amg.py#L158">源码</a>才弄清楚文档里所说的“the stability of the mask under changes to the cutoff used to binarize the model’s mask predictions”是什么意思。
下面展开介绍一下。</p>

<p>先要从<code class="language-plaintext highlighter-rouge">mask_threshold</code>说起。模型推理出来的是反映每个像素是否属于当前物体的对数几率值（logits），要通过<code class="language-plaintext highlighter-rouge">mask_threshold</code>将其二值化为0、1排布的掩膜。
<code class="language-plaintext highlighter-rouge">mask_threshold</code>默认值为0，是因为这些logits以0为中心（值域为[-20, 20]）。
如果增大或减小<code class="language-plaintext highlighter-rouge">mask_threshold</code>，部分接近0的logits（一般对应物体边界的像素）将产生不同的二值化结果，从而影响得到的掩膜。
所谓稳定，就是即使<code class="language-plaintext highlighter-rouge">mask_threshold</code>稍有增减，结果掩膜的变化也不大。我们可以通过计算变化前后掩膜区域的交并比来衡量稳定性，这就是稳定性分数。
计算时“稍有增减”的幅度通过<code class="language-plaintext highlighter-rouge">stability_score_offset</code>参数来指定（默认值为1.0）。</p>

<p>理解含义之后，就能通过调整这些接口参数来改变最终返回的掩膜数量。例如，要返回更多掩膜，可以减小<code class="language-plaintext highlighter-rouge">pred_iou_thresh</code>、<code class="language-plaintext highlighter-rouge">stability_score_thresh</code>、<code class="language-plaintext highlighter-rouge">stability_score_offset</code>，增大<code class="language-plaintext highlighter-rouge">box_nms_thresh</code>、<code class="language-plaintext highlighter-rouge">crop_nms_thresh</code>。当然，撒更多的点也行，不过推理计算量也就相应增加了。</p>]]></content><author><name></name></author><category term="技术" /><category term="AI" /><category term="Python" /><summary type="html"><![CDATA[Segment Anything Model (SAM) 是一个分割模型。它的接口参数只有很简单的文档说明。 我在使用中弄清楚了其含义，本文做个解释。如无特殊说明，这里提到的SAM是指其当前最新版本SAM 2。]]></summary></entry><entry><title type="html">PyTorch的分布式数据并行</title><link href="https://blog.shengbin.me/posts/pytorch-ddp" rel="alternate" type="text/html" title="PyTorch的分布式数据并行" /><published>2025-03-07T20:00:00+08:00</published><updated>2025-03-07T20:00:00+08:00</updated><id>https://blog.shengbin.me/posts/pytorch-ddp</id><content type="html" xml:base="https://blog.shengbin.me/posts/pytorch-ddp"><![CDATA[<p>分布式数据并行（Distributed Data Parallel）是PyTorch中的一个重要模块。</p>

<!--more-->

<p>所谓数据并行，就是将数据分为不同的部分，在不同计算节点（GPU卡，可以是一机多卡或是多机多卡）上并行处理各部分，然后将它们的处理结果进行汇总。</p>

<p>与数据并行相对的是模型并行（Model Parallel），也就是将模型网络切割为多个部分放在不同节点上，一般是为了解决模型参数规模太大的问题。</p>

<p>数据并行既可以用作训练，也可以用作推理或测试，不过大多数时候是为了加速训练。
下面主要介绍下采用PyTorch的DDP进行训练的方法和我实际踩过坑的一些经验。</p>

<p>PyTorch官网的<a href="https://pytorch.org/tutorials/intermediate/ddp_tutorial.html">一篇教程</a>展示了多种启动DDP训练的方式，其中甚至包含了和模型并行相结合的例子。不过我觉得最常用也最方便的是该文档<a href="https://pytorch.org/tutorials/intermediate/ddp_tutorial.html#initialize-ddp-with-torch-distributed-run-torchrun">最后一节</a>所介绍的，采用<code class="language-plaintext highlighter-rouge">torchrun</code>来发起训练：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>torchrun --nnodes=2 --nproc_per_node=8 --rdzv_id=100 --rdzv_backend=c10d --rdzv_endpoint=$MASTER_ADDR:29400 elastic_ddp.py
</code></pre></div></div>

<p>这里给出的命令是针对两个节点（两台服务器）、每节点8进程（每台服务器8张GPU卡）的场景，而且采用了<code class="language-plaintext highlighter-rouge">--rdzv_endpoint</code>这种新的方式来指定主机地址。这样在不同机器上运行同样命令即可。如果采用旧的方式，则需要在主从两台机器上运行不同的命令，如下所示（注意<code class="language-plaintext highlighter-rouge">--node_rank</code>的差异）：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># 第一台机器上
torchrun --nnodes=2 --nproc_per_node=8 --node_rank=0 --master_addr=$MASTER_ADDR elastic_ddp.py
# 第二台机器上
torchrun --nnodes=2 --nproc_per_node=8 --node_rank=1 --master_addr=$MASTER_ADDR elastic_ddp.py
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">elastic_ddp.py</code>是支持了DDP的训练脚本，教程中也给出了示例。实际应用中，除了模型需要用<code class="language-plaintext highlighter-rouge">torch.nn.parallel.DistributedDataParallel</code>来封装一下，数据加载也需要适配。
具体来说，就是要给<code class="language-plaintext highlighter-rouge">torch.utils.data.DataLoader</code>传入一个<code class="language-plaintext highlighter-rouge">torch.utils.data.DistributedSampler</code>，使得每个进程采样加载互不重复的部分数据。另外，这个训练脚本最好写得灵活一些，在单卡上或不通过torchrun也能直接运行（<code class="language-plaintext highlighter-rouge">python elastic_ddp.py</code>）。</p>

<p>我在单机上调好DDP后，要扩展到多机时，一开始总是无法成功运行。排查了好久才发现，原来是两个机器环境不完全一样导致的！
虽然我没找到有关版本兼容性的文档，但实验下来发现：不仅PyTorch版本需要一致，Python的版本也要完全一致。
保险起见，最好操作系统也用同一版本。于是我后来干脆也采用docker来部署训练了。</p>

<p>另一个遇到的问题是有台服务器配置了多个网络接口，而DDP没能自动选到合适的，无法建立通信。
这个倒是按照<a href="https://pytorch.org/docs/stable/distributed.html#choosing-the-network-interface-to-use">文档里的说明</a>，通过NCCL_SOCKET_IFNAME这一环境变量来手动指定一个网络就好了。</p>

<p>希望这些经验能对其他使用DDP的人有所帮助（尤其是多机运行时要确保环境完全相同这一点）。</p>]]></content><author><name></name></author><category term="技术" /><category term="PyTorch" /><category term="AI" /><summary type="html"><![CDATA[分布式数据并行（Distributed Data Parallel）是PyTorch中的一个重要模块。]]></summary></entry><entry><title type="html">股民老张</title><link href="https://blog.shengbin.me/posts/gu-min-lao-zhang" rel="alternate" type="text/html" title="股民老张" /><published>2025-02-28T08:00:00+08:00</published><updated>2025-02-28T08:00:00+08:00</updated><id>https://blog.shengbin.me/posts/gu-min-lao-zhang</id><content type="html" xml:base="https://blog.shengbin.me/posts/gu-min-lao-zhang"><![CDATA[<blockquote>
  <p>走啊，抄底去！</p>
</blockquote>

<!--more-->

<audio src="https://github.com/shengbin/storage/raw/refs/heads/main/gu-min-lao-zhang.mp3" type="audio/mpeg" preload="auto" autoplay="autoplay" controls="controls" loop="loop">
我去，你的浏览器竟然不支持HTML5？！赶紧去下个新版的吧。
</audio>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>九点半上岗 十五点离场
星期一到星期五天天都挺忙
炒股为哪桩 咱没太大理想
庄家要是吃了肉哇 跟着喝口汤
不涉抢和偷啊 不沾毒赌黄
买进卖出两头纳税拥护党中央
炒股票的感觉 究竟怎么样
听我给你仔细地说个端详

赚钱不容易 被套很平常
一年三百六十天经常是满仓
浅套快止损那 深套就死扛
四季转换风水轮流早晚被解放
股票一赚钱 心就有点慌
不知到底该了结还是该加仓
蒙上一匹大黑马那 那叫一个爽
一天一个涨停板 感觉忒膨胀

指数一横盘 是谁都没主张
不温不火不上不下 抻着牛皮糖
要问钱在哪呀嘿 就在你身旁
看不见摸不着就让你听个响
秋风吹又凉 大地一片黄
主力资金往外撤年底要结帐
飞流直下三千尺 一看是股指
挤泡沫的感觉 就是心尖拧的慌

研究基本面 不能傻算帐
银广夏和全国人民都敢耍花枪
蓝田股份 东方电子
造假能怎样
严打黑庄一审判 嗨
没见着吕梁
投资要理性 价值第一桩
ST的股票总是翻着倍地涨
资产重组老生长谈 年年月月讲哪
公司不仅卖业绩 还能卖想象

消息很重要 可咱耳朵不够长
报纸电视收音机外带互联网
股评家们两片嘴 左右都是理
红嘴黑嘴黄牙白牙 各唱各的腔

跟庄不入门 时间开了窗
坐在家里盯着K线 慢慢数波浪
江恩、布林、巴菲特呀
谁来帮帮我
金叉、死叉、KDJ
是越整越迷茫

这里没有地狱 没有天堂
这里不是赌场 也不是银行
离不开的股市 下不了的岗
这是我们发展中的证券市场
我来到这里的动机并不算高尚
我起得到的作用却能兴国安邦
揣着一分梦想和九分坚强
六千万里有我一位股民老张

我来到这里的动机并不算高尚
我起得到的作用却能兴国安邦
揣着一分梦想和九分坚强
六千万里有我一位股民老张
六千万里有我一位股民老张

走啊 抄底去
</code></pre></div></div>]]></content><author><name></name></author><category term="生活" /><category term="歌" /><category term="股票" /><summary type="html"><![CDATA[走啊，抄底去！]]></summary></entry><entry><title type="html">北欧神话</title><link href="https://blog.shengbin.me/posts/norse-mythology" rel="alternate" type="text/html" title="北欧神话" /><published>2025-02-16T20:00:00+08:00</published><updated>2025-02-16T20:00:00+08:00</updated><id>https://blog.shengbin.me/posts/norse-mythology</id><content type="html" xml:base="https://blog.shengbin.me/posts/norse-mythology"><![CDATA[<p>最近在玩游戏《战神·诸神黄昏》，为此特意去看了一些北欧神话的资料。下面做个简要的梳理。</p>

<!--more-->

<h2 id="世界">世界</h2>

<p>世界上存在五大种族：巨人、诸神、精灵、矮人、凡人。
巨人分为冰霜巨人、火巨人等。巨人是创世之初就产生的，代表着冰与火等自然力量。
诸神分为阿萨神族和华纳神族。这两族之间曾有战争，但最后和解。
精灵分为光明精灵和黑暗精灵，二者之间存在冲突。
矮人擅长锻造。
凡人就是我们人类自身。</p>

<p>世界分为九个界域，由世界树相连。
九界里位于中间的是中庭（米德加尔特），是凡人居住的地方。
中庭上方是亚尔夫海姆，是光明精灵的居所。再上方是阿斯加德，阿萨神族的领域。
中庭下方是斯瓦塔尔夫海姆，矮人国度；再下方是赫尔海姆，死人之国，也就是地狱。
中庭北方是尼福尔海姆，雾之国，极冷之地，浓雾笼罩，冰雪覆盖。
中庭南方是姆斯贝尔海姆，火之国，火焰巨人居住的地方，温度极高，其他种族难以进入。
中庭西方是华纳海姆，华纳神族的领地。
中庭东方是约顿海姆，巨人国度，此处有冰霜巨人和山地巨人，以前者为主。</p>

<h2 id="人物">人物</h2>

<p>阿萨神族首领奥丁，也是众神之王。其妻为弗丽嘉，有两个儿子巴德尔和霍德尔。奥丁还和众多女人外遇，并生下了其他一些子女。
奥丁和某个女巨人生下了神雷托尔。托尔妻子为希芙，有两个儿子曼尼、摩迪，一个女儿斯露德。
奥丁和另一个女巨人生下了战神提尔。也有说法认为提尔的父亲也是巨人。提尔曾周游各界，并受到九界的广泛拥戴。
奥丁与九大女神生下了海姆达尔。他守护着阿斯加德的入口彩虹桥。</p>

<p>奥丁还有个收养的儿子，名为洛基（另有一说洛基是奥丁的结拜兄弟）。
洛基从血缘上属于巨人，生母是女巨人劳菲。洛基与女巨人安格尔伯达生下了三个孩子：巨狼芬里尔、大蛇耶梦加得、冥界女神海拉。
其他的巨人还有：原初巨人尤弥尔，被奥丁战胜并杀死；密米尔，守护着智慧之井；史尔特尔，守护着火之国。</p>

<p>奥丁的妻子弗丽嘉也是华纳神族的女神芙蕾雅。芙蕾雅在两大神族战争和解后，和兄长弗雷一起来到阿萨神族做人质。</p>

<p>矮人和精灵主要服务于众神。矮人兄弟辛德里和布罗克铸造了托尔的雷神之锤。
此外，神族的很多宝物都是矮人所造，包括奥丁的武器永恒之枪、希芙的金发、弗雷的可折叠神船。</p>

<h2 id="诸神黄昏">诸神黄昏</h2>

<p>诸神黄昏是众多神族灭亡的大事件，其根源在于神族和巨人族之间多年的积怨，其前兆是芬布尔之冬。
导致这个漫长冬天来临的直接原因是奥丁之子、光明之神巴德尔的死亡。</p>

<p>巴德尔年幼时，母亲弗丽嘉为了保护他，令九界万物都发誓不得伤害巴德尔。
但这个禁令唯独漏掉了一种弱小的植物槲寄生。
后来，恶作剧之神洛基因为对巴德尔不满，诱使双目失明的黑暗之神霍德尔用槲寄生袭击巴德尔，导致了他的死亡。</p>

<p>巴德尔死后，九界开始陷入混乱，并最终在阿斯加德爆发了一场大决战。
巨狼芬里尔对战奥丁，大蛇耶梦加得对战雷神托尔，火焰巨人史尔特尔对战弗雷，地狱恶犬加姆对战提尔，洛基对战海姆达尔。
他们均在这场战斗中同归于尽。</p>

<p>最后，史尔特尔释放的火焰吞没了整个世界，这便是诸神黄昏。</p>

<p>以此为故事背景的游戏《战神·诸神黄昏》，是前作《战神》的续集。
这两部作品的主线讲述了主角奎托斯和他儿子阿特柔斯的经历，其中阿特柔斯的另一个名字就是洛基。</p>]]></content><author><name></name></author><category term="生活" /><category term="游戏" /><category term="总结" /><summary type="html"><![CDATA[最近在玩游戏《战神·诸神黄昏》，为此特意去看了一些北欧神话的资料。下面做个简要的梳理。]]></summary></entry></feed>