<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>GushanFall &#8211; 秋山砚语</title>
	<atom:link href="https://gsfall.cn/archives/author/admin/feed" rel="self" type="application/rss+xml" />
	<link>https://gsfall.cn</link>
	<description>GushanFall&#039;s Blog</description>
	<lastBuildDate>Mon, 02 Feb 2026 04:45:52 +0000</lastBuildDate>
	<language>zh-Hans</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9</generator>

<image>
	<url>https://gsfall.cn/wp-content/uploads/2025/04/cropped-icon-32x32.png</url>
	<title>GushanFall &#8211; 秋山砚语</title>
	<link>https://gsfall.cn</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Obsidian 进阶：手把手教你搭建“交互式”电影海报墙</title>
		<link>https://gsfall.cn/archives/169</link>
					<comments>https://gsfall.cn/archives/169#respond</comments>
		
		<dc:creator><![CDATA[GushanFall]]></dc:creator>
		<pubDate>Sun, 01 Feb 2026 08:09:01 +0000</pubDate>
				<category><![CDATA[Obsidian]]></category>
		<category><![CDATA[已分类]]></category>
		<category><![CDATA[Dataview]]></category>
		<category><![CDATA[教程]]></category>
		<guid isPermaLink="false">https://gsfall.cn/?p=169</guid>

					<description><![CDATA[为了更好地管理个人的观影记录，利用 DataviewJS 搭建了一个可视化的海报墙。相比于简单的文字列表，这个界面不仅更加直观，还具备了类似 App 的交互查询功能。]]></description>
										<content:encoded><![CDATA[
<p>为了更好地管理个人的观影记录，利用 <strong>DataviewJS</strong> 搭建了一个可视化的海报墙。相比于简单的文字列表，这个界面不仅更加直观，还具备了类似 App 的交互查询功能。</p>



<h1 class="wp-block-heading">最终效果展示</h1>



<figure class="wp-block-image size-large"><img fetchpriority="high" decoding="async" width="1024" height="936" src="https://gsfall.cn/wp-content/uploads/2026/02/image-1024x936.png" alt="" class="wp-image-170" srcset="https://gsfall.cn/wp-content/uploads/2026/02/image-1024x936.png 1024w, https://gsfall.cn/wp-content/uploads/2026/02/image-300x274.png 300w, https://gsfall.cn/wp-content/uploads/2026/02/image-768x702.png 768w, https://gsfall.cn/wp-content/uploads/2026/02/image-1536x1404.png 1536w, https://gsfall.cn/wp-content/uploads/2026/02/image.png 1700w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>摒弃了传统的表格形式，采用<strong>网格卡片布局</strong>来展示每一部电影。能够清晰展示以下核心信息：</p>



<ul class="wp-block-list">
<li><strong>视觉封面</strong>：自动加载高清电影海报。</li>



<li><strong>双重评分</strong>：同时展示“豆瓣评分”（大众参考）和“个人推荐”（私人喜好）。</li>



<li><strong>剧情与评价</strong>：简介和个人短评通过独立的滚动框展示，既保证了内容完整，又维持了界面的整洁统一。</li>
</ul>



<p>为了方便在庞大的片单中快速找到想看的内容，在顶部设计了四行<strong>动态工具栏</strong>：</p>



<ul class="wp-block-list">
<li><strong>排序功能</strong>：支持按“添加时间”、“上映年份”或“评分高低”一键排序。</li>



<li><strong>状态筛选</strong>：快速切换“已阅”库存和“想看”清单。</li>



<li><strong>分类检索</strong>：自动统计库中的热门标签（如科幻、悬疑），点击即可筛选对应类型的影片。</li>



<li><strong>年代回溯</strong>：可以按年份精准查找老片。</li>
</ul>



<h1 class="wp-block-heading">准备工作</h1>



<p>在开始编写代码之前，我们需要先搭建好基础环境，并统一电影笔记的格式。这一步至关重要，它决定了后续的脚本能否正确读取到数据。</p>



<h2 class="wp-block-heading">核心插件：Dataview</h2>



<p>这是整个影视库的引擎。我们需要通过它来索引笔记并渲染复杂的交互界面。</p>



<ul class="wp-block-list">
<li><strong>安装</strong>：在 Obsidian 社区插件市场搜索 <code>Dataview</code> 并安装启用。</li>



<li><strong>关键设置</strong>：打开 Dataview 设置面板，<strong>务必开启</strong> <code>Enable JavaScript Queries</code> 选项。</li>
</ul>



<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="434" src="https://gsfall.cn/wp-content/uploads/2026/02/image-1-1024x434.png" alt="" class="wp-image-171" srcset="https://gsfall.cn/wp-content/uploads/2026/02/image-1-1024x434.png 1024w, https://gsfall.cn/wp-content/uploads/2026/02/image-1-300x127.png 300w, https://gsfall.cn/wp-content/uploads/2026/02/image-1-768x325.png 768w, https://gsfall.cn/wp-content/uploads/2026/02/image-1.png 1185w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>注意</strong>：因为我们使用的是 <code>dataviewjs</code> 脚本，如果不开启此选项，代码将无法运行。</p>



<h2 class="wp-block-heading">建立目录</h2>



<p>建议为电影笔记建立一个独立的文件夹，例如 <code>Gallery/Movies</code>。 这样做的好处是方便脚本划定索引范围，避免扫描到无关的日常笔记，提高性能。</p>



<h2 class="wp-block-heading">标准化元数据（YAML）</h2>



<p>为了让脚本能识别电影的封面、评分和状态，每部电影的笔记都需要包含特定的 <strong>YAML Frontmatter</strong>（即笔记开头的属性区域）。</p>



<p>这是我目前使用的标准模板，可以将其保存为 Obsidian 的模板文件，每次新建电影时直接插入：</p>



<pre class="wp-block-code"><code>cover: "&#91;&#91;attachments/Pasted image 20260125144605.png]]"
title: 死无对证
original_title: The Invisible Witness
year: 2018
director: 斯蒂法诺·摩尔蒂尼
rating: 7.3
recommend: 4
status: 已看
tags:
  - 悬疑
  - 惊悚
  - 犯罪
plot: 阿德里亚诺是一名事业有成的企业家，直到那天他在酒店房间醒来，发现情人劳拉死在浴室里，而自己手握凶器。为了洗脱罪名，他请来了从未失手的金牌律师弗吉尼亚，试图在短短几小时内理清真相……
comment: 剧情复刻了《看不见的客人》，细节处理挺好，节奏紧凑，层层剥茧，最后的反转也非常精彩。《消失的她》应该是有借鉴这部影片。</code></pre>



<p><strong>关键字段说明</strong></p>



<ul class="wp-block-list">
<li><code>cover</code>: 封面文件的存放位置</li>



<li><code>rating</code>: 豆瓣评分，满分 10 分</li>



<li><code>recommend</code>: 个人推荐程度，满分 5 分</li>



<li><code>plot</code>: 电影简介</li>



<li><code>comment</code>: 个人评价</li>
</ul>



<h1 class="wp-block-heading">注入灵魂样式（CSS）</h1>



<p>有了数据之后，默认的 Dataview 输出还只是普通的列表。为了实现“海报墙”的视觉效果，我们需要通过 CSS 对页面元素进行重构。</p>



<p>这一步将决定影视库的“颜值”。</p>



<h2 class="wp-block-heading">启用 CSS 片段</h2>



<p>Obsidian 提供了非常方便的 <code>CSS Snippets</code>（代码片段）功能，允许我们在不修改主题的情况下自定义样式。</p>



<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="930" src="https://gsfall.cn/wp-content/uploads/2026/02/image-2-1024x930.png" alt="" class="wp-image-172" srcset="https://gsfall.cn/wp-content/uploads/2026/02/image-2-1024x930.png 1024w, https://gsfall.cn/wp-content/uploads/2026/02/image-2-300x272.png 300w, https://gsfall.cn/wp-content/uploads/2026/02/image-2-768x698.png 768w, https://gsfall.cn/wp-content/uploads/2026/02/image-2-1536x1395.png 1536w, https://gsfall.cn/wp-content/uploads/2026/02/image-2.png 1658w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<ul class="wp-block-list">
<li><strong>操作步骤</strong>：
<ol class="wp-block-list">
<li>进入 <code>设置</code> -> <code>外观</code> -> <code>CSS 代码片段</code>。</li>



<li>点击右侧的文件夹图标，打开 snippets 文件夹。</li>



<li>新建一个文本文件，重命名为 <code>media-cards.css</code>。</li>



<li>将下方的代码粘贴进去并保存。</li>



<li>回到 Obsidian 设置页，点击刷新按钮，并<strong>启用</strong> <code>media-cards.css</code>。</li>
</ol>
</li>
</ul>



<h2 class="wp-block-heading">样式代码</h2>



<pre class="wp-block-code"><code>/* --- media-cards.css (上下布局+两端对齐版) --- */

.media-gallery {
    display: grid;
    /* 保持较宽的卡片宽度 */
    grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
    gap: 20px; margin-top: 20px; padding-bottom: 20px;
}

.media-card {
    position: relative;
    background-color: var(--background-secondary);
    border-radius: 12px;
    border: 1px solid var(--background-modifier-border);
    transition: all 0.3s ease;
    display: flex; flex-direction: column;
    overflow: hidden;
    text-decoration: none !important; color: var(--text-normal) !important;
}
.media-card:hover {
    transform: translateY(-5px);
    box-shadow: 0 12px 24px rgba(0, 0, 0, 0.2);
    border-color: var(--interactive-accent);
}

/* 封面区域 */
.media-card-cover {
    width: 100%; aspect-ratio: 2 / 3; 
    background-size: cover; background-position: center top;
    position: relative; border-bottom: 1px solid var(--background-modifier-border);
}
.media-rank {
    position: absolute; top: 8px; left: 8px;
    background: rgba(0, 0, 0, 0.65); color: rgba(255, 255, 255, 0.9);
    padding: 2px 8px; border-radius: 6px; font-size: 0.75em; font-weight: 600;
    backdrop-filter: blur(4px);
}
.media-score-badge {
    position: absolute; bottom: 8px; right: 8px;
    background: var(--interactive-accent); color: var(--text-on-accent);
    padding: 2px 6px; border-radius: 4px; font-weight: 800; font-size: 0.9em;
    box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
/* 编辑按钮 */
.media-edit-btn {
    position: absolute; top: 8px; right: 8px; opacity: 0;
    transition: opacity 0.2s ease; background: rgba(var(--mono-rgb-100), 0.8);
    padding: 4px; border-radius: 50%; cursor: pointer; line-height: 1;
}
.media-card:hover .media-edit-btn { opacity: 1; }

/* 内容区域 */
.media-card-content {
    padding: 14px;
    flex-grow: 1; display: flex; flex-direction: column; 
    gap: 10px; /* 增加各模块之间的间距 */
}

/* 标题部分 */
.media-title-block {
    margin-bottom: 2px;
}
.media-title-cn { font-size: 1.1em; font-weight: 700; line-height: 1.3; color: var(--text-normal); margin-bottom: 2px; }
.media-title-en { 
    font-size: 0.8em; color: var(--text-muted); line-height: 1.2;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* --- 1. 评分行 (两端对齐) --- */
.media-rating-row {
    display: flex;
    justify-content: space-between; /* 关键：左边文字，右边星星 */
    align-items: center;
    font-size: 0.9em;
    height: 24px;
}
.media-label-rating {
    color: var(--text-muted);
    font-weight: bold;
}
/* 星星/红心容器 */
.media-stars-container {
    /* 强制使用 Emoji 字体，确保对齐 */
    font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
    letter-spacing: 2px;
}

/* --- 2. 文本块 (上下布局) --- */
.media-text-block {
    display: flex;
    flex-direction: column; /* 关键：垂直排列 */
    gap: 4px;
}
.media-label-text {
    font-size: 0.85em;
    font-weight: bold;
    color: var(--text-muted);
}
.media-text-content {
    background: var(--background-primary);
    padding: 8px;
    border-radius: 6px;
    font-size: 0.85em;
    color: var(--text-faint);
    line-height: 1.5;

    /* 高度控制 */
    height: 64px;       
    overflow-y: auto;
    word-wrap: break-word;
}
/* 滚动条 */
.media-text-content::-webkit-scrollbar { width: 3px; }
.media-text-content::-webkit-scrollbar-thumb {
    background-color: var(--text-muted); border-radius: 3px; opacity: 0.3;
}

/* 底部标签 */
.media-tags { margin-top: auto; display: flex; gap: 6px; flex-wrap: wrap; }
.media-tag {
    background: var(--background-modifier-hover); padding: 1px 6px;
    border-radius: 4px; font-size: 0.7em; color: var(--text-muted);
}
.media-meta-row {
    display: flex; justify-content: space-between; align-items: center;
    border-top: 1px solid var(--background-modifier-border); padding-top: 8px;
    font-size: 0.75em; color: var(--text-muted);
}
.media-meta-item {
    display: flex; align-items: center; gap: 4px;
    max-width: 60%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* --- 交互式工具栏样式 --- */

/* 工具栏容器 */
.media-toolbar {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
    margin-bottom: 16px;
    padding: 8px 12px;
    background: var(--background-secondary);
    border-radius: 8px;
    border: 1px solid var(--background-modifier-border);
    align-items: center;
}

/* 按钮组标签 (例如 "排序: ") */
.media-toolbar-label {
    font-size: 0.85em;
    font-weight: bold;
    color: var(--text-muted);
    margin-right: 4px;
}

/* 按钮本体 */
.media-btn {
    background: transparent;
    border: 1px solid var(--background-modifier-border);
    color: var(--text-muted);
    padding: 4px 10px;
    border-radius: 12px;
    font-size: 0.8em;
    cursor: pointer;
    transition: all 0.2s ease;

    /* 【新增】强制使用 Emoji 字体，解决按钮上红心变黑的问题 */
    font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
}

.media-btn:hover {
    background: var(--background-modifier-hover);
    color: var(--text-normal);
}

/* 激活状态的按钮 (高亮) */
.media-btn.active {
    background: var(--interactive-accent);
    color: var(--text-on-accent);
    border-color: var(--interactive-accent);
    font-weight: bold;
}</code></pre>



<p>当然这部分代码可以按照个人的喜好进行修改，如果不擅长代码，可以交给现在的大模型来修改，只需提出自己的要求。</p>



<h1 class="wp-block-heading">构建交互引擎（DataviewJS）</h1>



<p>在你的“影音库”笔记中（或者任意你想展示海报墙的笔记里），先添加属性，表明要使用前面启用的 css</p>



<pre class="wp-block-code"><code>cssclasses:
  - media-cards.css</code></pre>



<p>然后在添加 Dataview 代码块，并在代码块中粘贴以下代码</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="87" src="https://gsfall.cn/wp-content/uploads/2026/02/image-3-1024x87.png" alt="" class="wp-image-173" srcset="https://gsfall.cn/wp-content/uploads/2026/02/image-3-1024x87.png 1024w, https://gsfall.cn/wp-content/uploads/2026/02/image-3-300x26.png 300w, https://gsfall.cn/wp-content/uploads/2026/02/image-3-768x65.png 768w, https://gsfall.cn/wp-content/uploads/2026/02/image-3.png 1315w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<pre class="wp-block-code"><code>// ================= &#x2699; 配置区域 =================
const sourceFolder = '"Gallery/Movies"'; 
// ===============================================

// 1. 定义全局状态
let state = {
    sortKey: 'year',      // 排序依据
    sortOrder: 'desc',    // 排序方向
    filterTag: 'all',     // 标签筛选
    filterYear: 'all',    // 年份筛选
    filterStatus: 'all'   // 【新增】状态筛选 (all | watched | todo)
};

const root = dv.el("div", "", { cls: "media-app-container" });

// --- 图片处理 ---
function getImageUrl(imagePath) {
    if (!imagePath) return "";
    if (imagePath.path) {
        const file = app.metadataCache.getFirstLinkpathDest(imagePath.path, "");
        return file ? app.vault.getResourcePath(file) : "";
    }
    let str = String(imagePath);
    if (str.startsWith('!&#91;')) {
        let urlMatch = str.match(/\((https?:\/\/.*?)\)/);
        if (urlMatch) str = urlMatch&#91;1];
        else {
             let linkMatch = str.match(/\&#91;\&#91;(.*?)(?:\|.*)?\]\]/);
             if (linkMatch) str = linkMatch&#91;1];
        }
    }
    if (str.startsWith('http')) {
        return `https://images.weserv.nl/?url=${encodeURIComponent(str)}`;
    }
    let cleanName = str.replace(/&#91;\&#91;\]"]/g, '');
    const file = app.metadataCache.getFirstLinkpathDest(cleanName, "");
    return file ? app.vault.getResourcePath(file) : "";
}

function getHearts(index) {
    if (!index) return ""; 
    let score = Math.min(Math.max(index, 0), 5); 
    const emojiStyle = "font-family: 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', sans-serif;";
    return `&lt;span style="letter-spacing:2px; font-size:0.9em; ${emojiStyle}"&gt;` + "&#x2764;".repeat(score) + "&#x1f90d;".repeat(5 - score) + `&lt;/span&gt;`;
}

// 统计所有标签
function getTopTags(pages) {
    let tagMap = new Map();
    pages.forEach(p =&gt; {
        if (!p.tags) return;
        let tags = typeof p.tags === 'string' ? &#91;p.tags] : p.tags;
        tags.forEach(t =&gt; {
            if (t === 'Movie' || t === '电影' || t.includes('Media')) return;
            tagMap.set(t, (tagMap.get(t) || 0) + 1);
        });
    });
    return Array.from(tagMap.entries())
        .sort((a, b) =&gt; b&#91;1] - a&#91;1])
        // .slice(0, 8)
        .map(entry =&gt; entry&#91;0]);
}

// 提取所有存在的年份
function getYears(pages) {
    let years = new Set();
    pages.forEach(p =&gt; {
        if (p.year) years.add(p.year);
    });
    return Array.from(years).sort((a, b) =&gt; b - a);
}

// --- 渲染引擎 ---
function render() {
    root.innerHTML = "";

    // 1. 获取原始数据
    let allPages = dv.pages(sourceFolder)
        .filter(p =&gt; !p.file.name.includes("Template") &amp;&amp; p.file.name !== "未命名");

    const topTags = getTopTags(allPages);
    const allYears = getYears(allPages);

    // 2. 执行多重筛选 (Status + Tag + Year)
    let displayPages = allPages;

    // A. 【新增】状态筛选
    if (state.filterStatus === 'watched') {
        displayPages = displayPages.filter(p =&gt; {
            const s = (p.status || p.状态 || "").toLowerCase();
            return s.includes('watched') || s.includes('已阅') || s.includes('已看');
        });
    } else if (state.filterStatus === 'todo') {
        displayPages = displayPages.filter(p =&gt; {
            const s = (p.status || p.状态 || "").toLowerCase();
            return s.includes('wish') || s.includes('todo') || s.includes('想看');
        });
    }

    // B. 标签筛选
    if (state.filterTag !== 'all') {
        displayPages = displayPages.filter(p =&gt; {
            if (!p.tags) return false;
            let tags = typeof p.tags === 'string' ? &#91;p.tags] : p.tags;
            return tags.includes(state.filterTag);
        });
    }

    // C. 年份筛选
    if (state.filterYear !== 'all') {
        displayPages = displayPages.filter(p =&gt; p.year == state.filterYear);
    }

    // 3. 执行排序
    if (state.sortKey === 'year') {
        displayPages = displayPages.sort(p =&gt; p.year, state.sortOrder);
    } else if (state.sortKey === 'rating') {
        displayPages = displayPages.sort(p =&gt; p.rating, state.sortOrder);
    } else if (state.sortKey === 'recommend') {
        displayPages = displayPages.sort(p =&gt; (p.recommend || p&#91;"推荐指数"] || 0), state.sortOrder);
    } else if (state.sortKey === 'date') {
        displayPages = displayPages.sort(p =&gt; p.file.ctime, state.sortOrder);
    } else {
        displayPages = displayPages.sort(p =&gt; p.file.name, state.sortOrder);
    }

    // 4. 绘制工具栏
    const toolbar = document.createElement("div");
    toolbar.className = "media-toolbar";
    toolbar.style.flexDirection = "column"; 
    toolbar.style.alignItems = "flex-start";

    // --- 第一行：排序控制 ---
    const rowSort = createToolbarRow("排序:");
    createBtn(rowSort, "&#x1f552; 添加时间", () =&gt; { state.sortKey = 'date'; render(); }, state.sortKey === 'date');
    createBtn(rowSort, "&#x1f4c5; 上映年份", () =&gt; { state.sortKey = 'year'; render(); }, state.sortKey === 'year');
    createBtn(rowSort, "&#x2b50; 评分", () =&gt; { state.sortKey = 'rating'; render(); }, state.sortKey === 'rating');
    createBtn(rowSort, "&#x2764; 推荐", () =&gt; { state.sortKey = 'recommend'; render(); }, state.sortKey === 'recommend');

    const spacer = document.createElement("span"); spacer.style.flexGrow = "1"; rowSort.appendChild(spacer);
    const orderText = state.sortOrder === 'desc' ? "&#x2b07; 降序" : "&#x2b06; 升序";
    createBtn(rowSort, orderText, () =&gt; { state.sortOrder = state.sortOrder === 'desc' ? 'asc' : 'desc'; render(); }, false);
    toolbar.appendChild(rowSort);

    // --- 第二行：状态筛选 【新增】 ---
    const rowStatus = createToolbarRow("状态:");
    createBtn(rowStatus, "全部", () =&gt; { state.filterStatus = 'all'; render(); }, state.filterStatus === 'all');
    createBtn(rowStatus, "&#x2705; 已看", () =&gt; { state.filterStatus = 'watched'; render(); }, state.filterStatus === 'watched');
    createBtn(rowStatus, "&#x1f4dd; 想看", () =&gt; { state.filterStatus = 'todo'; render(); }, state.filterStatus === 'todo');
    toolbar.appendChild(rowStatus);

    // --- 第三行：标签筛选 ---
    const rowTag = createToolbarRow("类型:");
    createBtn(rowTag, "全部", () =&gt; { state.filterTag = 'all'; render(); }, state.filterTag === 'all');
    topTags.forEach(tag =&gt; {
        createBtn(rowTag, tag, () =&gt; { state.filterTag = tag; render(); }, state.filterTag === tag);
    });
    toolbar.appendChild(rowTag);

    // --- 第四行：年份筛选 ---
    const rowYear = createToolbarRow("年份:");
    createBtn(rowYear, "全部", () =&gt; { state.filterYear = 'all'; render(); }, state.filterYear === 'all');
    allYears.forEach(y =&gt; {
        createBtn(rowYear, String(y), () =&gt; { state.filterYear = y; render(); }, state.filterYear == y);
    });
    toolbar.appendChild(rowYear);

    root.appendChild(toolbar);

    // 5. 绘制卡片
    if (displayPages.length === 0) {
        root.createEl("p", { text: "&#x26a0; 没有找到符合条件的电影", style: "color:var(--text-muted); padding:20px;" });
        return;
    }

    const container = document.createElement("div");
    container.className = "media-gallery"; 

    displayPages.forEach((page) =&gt; {
        let coverUrl = getImageUrl(page.cover || page&#91;"封面"]);
        let title = page.title || page&#91;"中文名"] || page.file.name;
        let subTitle = page.original_title || page&#91;"英文名"] || ""; 
        let rating = page.rating || page&#91;"评分"] || 0;
        let year = page.year || page&#91;"年份"] || "-";
        let director = page.director || page&#91;"导演"] || "未知";
        let status = page.status || page&#91;"状态"] || "已阅";
        let plot = page.plot || page&#91;"剧情"] || page&#91;"剧情简介"];
        let comment = page.comment || page&#91;"评价"] || page&#91;"个人评价"];
        let recommendIndex = page.recommend || page&#91;"推荐指数"] || 0;
        let displayTags = page.tags || &#91;];
        if (typeof displayTags === 'string') displayTags = &#91;displayTags];

        const tagsHtml = displayTags
            .filter(t =&gt; !t.includes("Movie") &amp;&amp; !t.includes("电影")) 
            .slice(0, 3) 
            .map(t =&gt; `&lt;span class="media-tag"&gt;${t}&lt;/span&gt;`)
            .join('');

        const card = document.createElement("div");
        card.className = "media-card";

        card.innerHTML = `
            &lt;div class="media-card-cover" style="background-image: url('${coverUrl}');"&gt;
                &lt;div class="media-rank"&gt;${year}&lt;/div&gt;
                &lt;div class="media-score-badge"&gt;${rating}&lt;/div&gt;
                &lt;div class="media-edit-btn" title="打开笔记"&gt;&#x270f;&lt;/div&gt;
            &lt;/div&gt;

            &lt;div class="media-card-content"&gt;
                &lt;div class="media-title-block"&gt;
                    &lt;div class="media-title-cn"&gt;${title}&lt;/div&gt;
                    &lt;div class="media-title-en"&gt;${subTitle}&lt;/div&gt;
                &lt;/div&gt;

                ${recommendIndex &gt; 0 ? `
                &lt;div class="media-rating-row"&gt;
                    &lt;span class="media-label-rating"&gt;推荐指数&lt;/span&gt;
                    &lt;span class="media-stars-container"&gt;${getHearts(recommendIndex)}&lt;/span&gt;
                &lt;/div&gt;` : ''}

                ${plot ? `
                &lt;div class="media-text-block"&gt;
                    &lt;div class="media-label-text"&gt;剧情简介&lt;/div&gt;
                    &lt;div class="media-text-content"&gt;${plot}&lt;/div&gt;
                &lt;/div&gt;` : ''}

                ${comment ? `
                &lt;div class="media-text-block"&gt;
                    &lt;div class="media-label-text"&gt;个人评价&lt;/div&gt;
                    &lt;div class="media-text-content"&gt;${comment}&lt;/div&gt;
                &lt;/div&gt;` : ''}

                &lt;div class="media-tags"&gt;${tagsHtml}&lt;/div&gt;

                &lt;div class="media-meta-row"&gt;
                    &lt;div class="media-meta-item" title="导演"&gt;&#x1f3ac; ${director}&lt;/div&gt;
                    &lt;div class="media-meta-item" title="状态"&gt;&#x1f3f7; ${status}&lt;/div&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        `;
        card.onclick = (e) =&gt; {
            e.preventDefault();
            app.workspace.openLinkText(page.file.path, "", false);
        };
        container.appendChild(card);
    });
    root.appendChild(container);
}

// 辅助：创建工具栏的一行
function createToolbarRow(labelText) {
    const row = document.createElement("div");
    row.style.display = "flex";
    row.style.gap = "8px";
    row.style.width = "100%";
    row.style.marginBottom = "8px";
    row.style.borderBottom = "1px solid var(--background-modifier-border)";
    row.style.paddingBottom = "8px";
    row.style.flexWrap = "wrap";

    const label = document.createElement("span");
    label.className = "media-toolbar-label";
    label.innerText = labelText;
    row.appendChild(label);
    return row;
}

function createBtn(parent, text, onClick, isActive) {
    const btn = document.createElement("button");
    btn.className = "media-btn" + (isActive ? " active" : "");
    btn.innerText = text;
    btn.onclick = onClick;
    parent.appendChild(btn);
}

render();</code></pre>



<p>请务必修改代码第一行的 <code>sourceFolder</code>，将其替换为你存放电影笔记的实际文件夹路径（例如 <code>"MyData/Movies"</code>）。</p>



<p>如果一切正常，当你退出代码编辑模式，切换到<strong>预览模式（Reading View）</strong>时，你应该能看到：</p>



<ol class="wp-block-list">
<li>顶部出现了四行工具栏（排序、状态、类型、年份）。</li>



<li>下方的卡片整齐排列，海报图片已经加载出来。</li>



<li>点击顶部的按钮，下方的卡片会即时发生变化。</li>
</ol>



<p><strong><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2728.png" alt="✨" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 至此，你的个人交互式影视库就已经搭建完成了！</strong></p>



<p></p>
]]></content:encoded>
					
					<wfw:commentRss>https://gsfall.cn/archives/169/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>FlashAttention 原理详解与代码实现</title>
		<link>https://gsfall.cn/archives/93</link>
					<comments>https://gsfall.cn/archives/93#respond</comments>
		
		<dc:creator><![CDATA[GushanFall]]></dc:creator>
		<pubDate>Sat, 10 May 2025 03:54:39 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<category><![CDATA[DeepLearning]]></category>
		<category><![CDATA[Papers]]></category>
		<category><![CDATA[已分类]]></category>
		<category><![CDATA[Attention]]></category>
		<category><![CDATA[FlashAttention]]></category>
		<guid isPermaLink="false">https://gsfall.cn/?p=93</guid>

					<description><![CDATA[Transformer 模型在自然语言处理和图像分类等领域被广泛应用，但其核心的自注意力模块在处理长序列时面临时间和内存复杂度呈二次方增长的问题，限制了模型对更长上下文的处理能力。尽管已有许多近似注意力方法试图通过降低计算复杂度来解决这一问题，但这些方法往往未能显著提升实际运行速度，且可能牺牲模型性能。而 FlashAttention 在提高运行速度的同时，也保留了计算精度。]]></description>
										<content:encoded><![CDATA[
<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading">论文简析</h1>



<p>原论文：<a href="FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness">FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness</a></p>



<h2 class="wp-block-heading">研究背景</h2>



<p>Transformer 模型在自然语言处理和图像分类等领域被广泛应用，但其核心的自注意力模块在处理长序列时面临时间和内存复杂度呈二次方增长的问题，限制了模型对更长上下文的处理能力。尽管已有许多近似注意力方法试图通过降低计算复杂度来解决这一问题，但这些方法往往未能显著提升实际运行速度，且可能牺牲模型性能。</p>



<h2 class="wp-block-heading">新方法</h2>



<p>本文提出了一种新的注意力算法 FLASHATTENTION，旨在通过减少 GPU 内存访问次数（即 I/O ）来显著提高 Transformer 模型在长序列上的运行速度和内存效率，同时保持注意力计算的精确性。</p>


<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><img loading="lazy" decoding="async" width="980" height="593" src="https://gsfall.cn/wp-content/uploads/2025/05/FlashAttention-f1.png" alt="" class="wp-image-101" style="width:680px;height:auto" srcset="https://gsfall.cn/wp-content/uploads/2025/05/FlashAttention-f1.png 980w, https://gsfall.cn/wp-content/uploads/2025/05/FlashAttention-f1-300x182.png 300w, https://gsfall.cn/wp-content/uploads/2025/05/FlashAttention-f1-768x465.png 768w" sizes="auto, (max-width: 980px) 100vw, 980px" /></figure>
</div>


<p><strong>FLASHATTENTION 方法</strong>：</p>



<ol class="wp-block-list">
<li><strong>分块（Tiling）</strong>：将输入矩阵 Q、K、V 分成小块，逐块加载到 GPU 的快速片上 SRAM 中进行计算，避免一次性将整个大矩阵加载到较慢的 HBM 中。</li>



<li><strong>重计算（Recomputation）</strong>：在反向传播中，通过存储前向传播中的 softmax 归一化因子，利用这些因子在 SRAM 中快速重新计算注意力矩阵，而不是从 HBM 中读取中间结果，从而减少 HBM 访问次数。</li>



<li><strong>融合操作（Kernel Fusion）</strong>：将所有注意力操作融合到一个 GPU 内核中，避免多次从 HBM 读取输入和写入输出，进一步减少内存访问开销。</li>



<li><strong>扩展到稀疏注意力</strong>：进一步将 FLASHATTENTION 扩展为块稀疏版本，通过仅计算非零块的注意力矩阵，进一步降低内存访问次数和计算复杂度。</li>
</ol>



<h2 class="wp-block-heading">总结</h2>



<p>本文提出了 FLASHATTENTION，一种针对 Transformer 模型的高效精确注意力算法。通过分块、重计算和融合操作，FLASHATTENTION 显著减少了 GPU 内存访问次数，从而在长序列上实现了更快的训练速度和更低的内存占用。实验表明，该方法不仅加速了模型训练，还通过扩展上下文长度提高了模型性能，为 Transformer 模型在长序列任务中的应用提供了新的可能性。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading">标准 Attention </h1>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="971" height="191" src="https://gsfall.cn/wp-content/uploads/2025/05/image.png" alt="" class="wp-image-105" srcset="https://gsfall.cn/wp-content/uploads/2025/05/image.png 971w, https://gsfall.cn/wp-content/uploads/2025/05/image-300x59.png 300w, https://gsfall.cn/wp-content/uploads/2025/05/image-768x151.png 768w" sizes="auto, (max-width: 971px) 100vw, 971px" /></figure>
</div>


<p>其中$Q, K, V$ 都是形状为 $(N\times d)$ 的矩阵， $S, P$ 是形状为 $(N\times N)$ 的矩阵。</p>



<p>从而可知：</p>



<ol class="wp-block-list">
<li>如果要在只读写一次 HBM 的情况下完成整个计算，SRAM 中至少需要 $O(Nd+N^2)$ 的空间（保存QKV，注意力矩阵以及中间计算结果），但在实际应用中序列长度 $N$ 通常非常大，导致显存需求远超 SRAM 容量。</li>



<li>所以在标准 Attention 的计算中，需要频繁地访问 HBM 以获取计算数据，I/O 需求为 $O(Nd+N^2)$ ，与序列长度的平方相关。</li>
</ol>



<p>一般来说，一个程序的执行速度瓶颈有两类：计算瓶颈与内存瓶颈。而在标准 Attention 中，计算效率的瓶颈正是频繁的显存访问（HBM I/O）。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading">分块 Softmax 计算</h1>



<p>在正式开始介绍 FlashAttention 之前，需要先了解 Softmax。因为在其分块计算的过程中，最为复杂的就是如何分块计算 Softmax 。</p>



<h2 class="wp-block-heading">Softmax</h2>



<p>Softmax 函数是机器学习和深度学习中广泛使用的归一化指数函数，主要用于将任意实数向量转换为概率分布，其计算公式如下：</p>



<p>$$\text{Softmax}(x_i)=\frac{e^{x_i}}{\sum^{n}_{j}e^{x^j}}$$</p>



<p>其中：</p>



<ol class="wp-block-list">
<li>对输入进行指数变换，以放大元素间差异。</li>



<li>分母为归一化因子，以确保输出的所有元素和为 1。</li>
</ol>



<h2 class="wp-block-heading">Safe Softmax (3-Pass)</h2>



<p>在计算 Softmax 时，即使数据类型为 FP32，当 $x_i=89$ 时，分子$e^{x_i}$ 已经超过了 FP32 的范围。Safe Softmax 通过减去$x_i$中的最大值，来避免数据溢出，其公式如下：</p>



<p>$$\text{Safe-Softmax}(x_i)=\frac{e^{x_i-m}}{\sum^{n}_{j}e^{x_j-m}}, m=\max(x_1,x_2,\ldots,x_n)$$</p>



<p>因为 $x_i-m\le 0$ ，所以避免了分子数据溢出。</p>



<p>python 简单实现如下：</p>



<pre class="wp-block-code"><code># Safe Softmax (3 Pass)
import math

def safe_softmax_3pass(x):
    # 找到全局最大值 m 
    m = float('-inf')
    for i in range(len(x)):
        m = max(m, x&#91;i])
    
    # 计算分母归一化指数的和 d
    d = 0
    for i in range(len(x)):
        d += math.exp(x&#91;i] - m)
        
    # 计算 softmax 的值
    a = &#91;0 for _ in range(len(x))]
    for i in range(len(x)):
        a&#91;i] = (math.exp(x&#91;i] - m) / d)

    return a</code></pre>



<p>当输入为 [1, 2, 3, 4] 时，输出为：</p>



<pre class="wp-block-code"><code>output = safe_softmax_3pass(&#91;1, 2, 3, 4])
print(f"Output:{output}")
# Output:&#91;0.03205860328008499, 0.08714431874203257, 0.23688281808991013, 0.6439142598879724]</code></pre>



<p>不难看出，整个计算需要经过三次遍历。且若没有足够的 SRAM 的空间存下所有数据，则每次遍历都需要从 HBM 中读取相应数据，增加 I/O 访问。</p>



<h2 class="wp-block-heading">Safe Softmax (2-Pass)</h2>



<p>通过合并前两步的计算，可以减少遍历次数，从而加快计算速度。</p>



<p>第一次遍历 ($i: 1 \rightarrow N$)：</p>



<p>$$m_i = \max(m_{i &#8211; 1}, x_i)$$</p>



<p>$$d_i = d_{i-1}\cdot e^{m_{i-1}-m_i}+e^{x_i-m_i}$$</p>



<p>第二次遍历 ($i: 1 \rightarrow N$)：</p>



<p>$$a_i=\frac{e^{x_i-m_N}}{d_N}$$</p>



<p>证明如下：</p>



<ol class="wp-block-list">
<li>若 $x_i\le m_{i-1}$，即前 $i$ 项的最大值没有变化，$m_i = m_{i-1}$，所以 $d_i$ 只需要加上第 $i$ 项的归一化指数</li>



<li>若 $x_i\gt m_{i-1}$，即第 $i$ 项才是当前最大值，$m_i=x_i$，所以前 $i-1$ 项的归一化指数的计算需要更新，需要由 $e^{x_j-m_{i-1}}$ 更新为 $e^{x_j-m_i}$，即指数需要多减去差值$m_i-m_{i-1}$
<ul class="wp-block-list">
<li>$$d_{i-1}&#8217;=d_{i-1}\cdot e^{-(m_i-m_{i-1})}=d_{i-1}\cdot e^{m_{i-1}-m_i}$$</li>



<li>$$d_i = d_{i-1}&#8217;+e^{x_i-m_i}=d_{i-1}\cdot e^{m_{i-1}-m_i}+e^{x_i-m_i}$$</li>
</ul>
</li>
</ol>



<p>python 代码如下：</p>



<pre class="wp-block-code"><code># Safe Softmax (2 Pass)
import math

def safe_softmax_2pass(x):
    # 每次更新最大值与分母归一化指数项的和
    m = float('-inf')
    d = 0
    for i in range(len(x)):
        m_new = max(m, x&#91;i])
        d = d * math.exp(m - m_new) + math.exp(x&#91;i] - m_new)
        m = m_new
    
    # 计算 softmax 的值
    a = &#91;0 for _ in range(len(x))]
    for i in range(len(x)):
        a&#91;i] = math.exp(x&#91;i] - m) / d

    return a</code></pre>



<p>当输入为 [1, 2, 3, 4] 时，输出为</p>



<pre class="wp-block-code"><code>output = safe_softmax_2pass(&#91;1, 2, 3, 4])
print(f"Output:{output}")
# Output:&#91;0.03205860328008499, 0.08714431874203257, 0.23688281808991013, 0.6439142598879724]</code></pre>



<p>结果与上一节的 3-pass 一致。</p>



<h2 class="wp-block-heading">分块 Softmax</h2>



<p>FlashAttention 算法中分块 Softmax 的计算思路与 Safe Softmax (2-Pass) 是一致的，只是更新的频率从每一项变成了每一块。</p>



<ol class="wp-block-list">
<li>计算第 $i$ 块的局部最大值 $\tilde{m}_{ij}$，并根据该局部最大值求得每一项的归一化指数和 $\tilde{\ell}_{ij}$。</li>



<li>计算前 $i$ 块的最大值 $m_i^{\mathrm{new}}=\max(m_i,\tilde{m}_{ij})$。</li>



<li>更新前 $i$ 块的归一化指数和（与 2-Pass 的思路一致） 。</li>
</ol>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="299" height="35" src="https://gsfall.cn/wp-content/uploads/2025/05/flash_attn_softmax.png" alt="" class="wp-image-139"/></figure>
</div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading">FlashAttention</h1>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="982" height="637" src="https://gsfall.cn/wp-content/uploads/2025/05/flash_attn_flashattn.png" alt="" class="wp-image-149" srcset="https://gsfall.cn/wp-content/uploads/2025/05/flash_attn_flashattn.png 982w, https://gsfall.cn/wp-content/uploads/2025/05/flash_attn_flashattn-300x195.png 300w, https://gsfall.cn/wp-content/uploads/2025/05/flash_attn_flashattn-768x498.png 768w" sizes="auto, (max-width: 982px) 100vw, 982px" /></figure>
</div>


<h2 class="wp-block-heading">分块（step 1-4）</h2>



<p>假设 $\mathbf{Q},\mathbf{K},\mathbf{V}\in\mathbb{R}^{N\times d}$ 在 HBM 上。</p>



<h3 class="wp-block-heading">step 1 确定分块大小</h3>



<p>$B_{c}=\lceil\frac{M}{4d}\rceil$ ：$B_{c}$ 是 $\mathbf{K},\mathbf{V}$ 的列分块大小。向上取整是为了分更多的块，从而确保 SRAM 能同时存下计算所需的数据。$M$ 是 SRAM 的大小，除以 $4d$ 是因为需要同时存放 $\mathbf{Q},\mathbf{K},\mathbf{V},\mathbf{O}$。</p>



<p>$B_{r}=\min(\lceil\frac{M}{4d}\rceil,d)$：$B_{r}$ 是 $\mathbf{Q}$ 的行分块大小，并限制不超过 $d$，这可以保证分块后的 $\mathbf{Q}_i$ 在计算时能够存放中间计算结果 $\mathbf{S}_ij$。</p>



<h3 class="wp-block-heading">step 2 预留结果暂存空间</h3>



<p>$\mathbf{O}$ 是最后的输出，$\ell$ 是计算 Softmax 时的归一化指数和， $m$ 是每一块以及之前块的最大值。</p>



<h3 class="wp-block-heading">step 3-4 将矩阵分块</h3>



<p>$T_c$ 为 $\mathbf{K},\mathbf{V}$ 的分块数量。</p>



<p>$T_r$ 为 $\mathbf{Q}$ 的分块数量，同时也是 $\mathbf{O},\ell,m$ 的分块数量。</p>



<h2 class="wp-block-heading">双循环计算（step 5-15）</h2>



<h3 class="wp-block-heading">内循环</h3>



<p>$\mathbf{Q}_1\rightarrow \mathbf{Q}_{T_r}$：每次加载新的 $\mathbf{Q}_i$，而$\mathbf{K}_j,\mathbf{V}_j$ 不变。</p>



<ol class="wp-block-list">
<li>计算注意力分数$\mathbf{S}<em>{ij}</em>$（step 9）</li>



<li>分块 softmax，前面已经讲过 （step 10-11)</li>



<li>更新 $\mathbf{O}_i$（step 12）：这里前一项可以写成 $(\ell_i\mathbf{O}_i)\frac{e^{m_i-m_i^{new}}}{\ell_i^{new}}$，即先将前 $i-1$ 块的和还原，再重新计算新的结果，然后再加上第 $i$ 块的结果</li>



<li>更新 $\ell_i,m_i$ （step 13）</li>
</ol>



<h3 class="wp-block-heading">外循环</h3>



<p>$\mathbf{K}_1,\mathbf{V}_1\rightarrow \mathbf{K}_{T_c},\mathbf{V}_{T_c}$：加载新的 $\mathbf{K}_j,\mathbf{V}_j$，并再次遍历 $\mathbf{Q}$ 的每一块进行计算</p>



<h3 class="wp-block-heading">示意图</h3>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="982" src="https://gsfall.cn/wp-content/uploads/2025/05/flash_attn_draw-1024x982.png" alt="" class="wp-image-158" srcset="https://gsfall.cn/wp-content/uploads/2025/05/flash_attn_draw-1024x982.png 1024w, https://gsfall.cn/wp-content/uploads/2025/05/flash_attn_draw-300x288.png 300w, https://gsfall.cn/wp-content/uploads/2025/05/flash_attn_draw-768x737.png 768w, https://gsfall.cn/wp-content/uploads/2025/05/flash_attn_draw-1536x1473.png 1536w, https://gsfall.cn/wp-content/uploads/2025/05/flash_attn_draw-2048x1964.png 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<h2 class="wp-block-heading">代码实现</h2>



<pre class="wp-block-code"><code># 手动实现 FlashAttention 的计算，不涉及反向传播

import torch
import math

# 论文中 QKV 的形状为 (N, d)，对应到这里为 (seq_len, head_dim)
def my_flash_attention(query, key, value, mask=None):
    # 负无穷大
    neg_inf = float('-inf')
    # epsilon 防止除零
    epsilon = 1e-6
    
    # N，d
    seq_len = query.size(-2)
    head_dim = query.size(-1)
    
    # 预留 output
    output = torch.zeros_like(query, device=query.device, dtype=torch.float16)
    
    # 记录分块 softmax 中的最大值
    m = torch.ones(query.shape&#91;:-1], device=query.device, dtype=torch.float16)&#91;..., None] * neg_inf
    # 记录分块 softmax 中的和
    l = torch.zeros(query.shape&#91;:-1], device=query.device, dtype=torch.float16)&#91;..., None]
    
    # KV 的列分块大小，论文中由 M 决定，此处暂取为固定值
    B_c = 4
    # Q 的行分块大小
    B_r = min(B_c, head_dim)
    # KV 的分块数量
    T_c = math.ceil(seq_len / B_c)
    # Q 的分块数量
    T_r = math.ceil(seq_len / B_r)
    
    # 将 QKV 分块
    query_blocks = torch.split(query, B_r, dim=-2)
    key_blocks = torch.split(key, B_c, dim=-2)
    value_blocks = torch.split(value, B_c, dim=-2)
    
    # mask分块
    mask_blocks = list(torch.split(mask, B_c, dim=-1))
    
    # 将 output、m、l分块
    ouput_blocks = list(torch.split(output, B_r, dim=-2))
    m_blocks = list(torch.split(m, B_r, dim=-2))
    l_blocks = list(torch.split(l, B_r, dim=-2))
    
    # 分块计算注意力
    # 外循环：j -> T_c
    for j in range(T_c):
        key_j = key_blocks&#91;j]
        value_j = value_blocks&#91;j]
        mask_j = mask_blocks&#91;j]
        # 内循环：i -> T_r
        for i in range(T_r):
            query_i = query_blocks&#91;i]
            output_i = ouput_blocks&#91;i]
            m_i = m_blocks&#91;i]
            l_i = l_blocks&#91;i]
            
            # 计算 Q@K^T/sqrt(d_k)
            S_ij = torch.matmul(query_i, key_j.transpose(-2, -1)) / (head_dim ** 0.5)
            
            # mask
            if mask_j is not None:
                S_ij = S_ij.masked_fill(mask_j.unsqueeze(1) == 0, float('-inf'))
            
            # 分块 softmax
            m_ij, _ = torch.max(S_ij, dim=-1, keepdim=True)
            P_ij = torch.exp(S_ij - m_ij)
            l_ij = torch.sum(P_ij, dim=-1, keepdim=True) + epsilon
            # 更新最大值
            m_i_new = torch.max(m_i, m_ij)
            l_i_new = torch.exp(m_i - m_i_new) * l_i + torch.exp(m_ij - m_i_new) * l_ij
            
            # 计算并更新 output
            ouput_blocks&#91;i] = (l_i * torch.exp(m_i - m_i_new) * output_i + torch.exp(m_ij - m_i_new) * torch.matmul(P_ij, value_j)) / l_i_new
            
            # 更新 m、l
            m_blocks&#91;i] = m_i_new
            l_blocks&#91;i] = l_i_new
            
    # 拼接 output
    output = torch.cat(ouput_blocks, dim=-2)
    # 拼接 m、l
    m = torch.cat(m_blocks, dim=-2)
    l = torch.cat(l_blocks, dim=-2)
    
    return output</code></pre>



<h2 class="wp-block-heading">结果验证</h2>



<h3 class="wp-block-heading">参考值</h3>



<p>生成随机的 Q，K，V</p>



<pre class="wp-block-code"><code>import math
import torch
import torch.nn as nn
from torch.nn import functional as F
from torch.nn.attention import sdpa_kernel, SDPBackend

# 设置随机种子
torch.manual_seed(1)

# 定义输入参数
batch_size = 1
seq_len = 10
embed_dim = 6
num_heads = 2
head_dim = embed_dim // num_heads

# 随机生成输入张量 (batch_size, num_heads, seq_len, head_dim)
query = torch.randn(batch_size, num_heads, seq_len, head_dim, device='cuda', dtype=torch.float16)
key = torch.randn(batch_size, num_heads, seq_len, head_dim, device='cuda', dtype=torch.float16)
value = torch.randn(batch_size, num_heads, seq_len, head_dim, device='cuda', dtype=torch.float16)

# 随机生成注意力掩码 (batch_size, seq_len)
mask = torch.randint(0, 2, (batch_size, seq_len), device='cuda', dtype=torch.float16)</code></pre>



<p>在 PyTorch 中有一个函数为 <strong>scaled_dot_product_attention</strong>，它有三种实现方式</p>



<ol class="wp-block-list">
<li>FlashAttention</li>



<li>标准数学实现</li>



<li>内存高效注意力</li>
</ol>



<h4 class="wp-block-heading">标准数学实现</h4>



<p>在这里，我们通过上下文控制器来调用其 FlashAttention 与标准数学实现</p>



<pre class="wp-block-code"><code># 标准数学实现
with sdpa_kernel(SDPBackend.MATH):
    output_pytorch_flash = F.scaled_dot_product_attention(
        query, key, value,
        attn_mask=None,
        dropout_p=0.0,
        is_causal=False
    )

# Output: (batch_size, num_heads, seq_len, head_dim)
print(f"Shape:{output_pytorch_flash.shape}")
print(output_pytorch_flash)</code></pre>



<p>标准数学实现输出结果</p>



<pre class="wp-block-code"><code>Shape:torch.Size(&#91;1, 2, 10, 3])
tensor(&#91;&#91;&#91;&#91; 0.4641,  0.1559, -0.4280],
          &#91; 0.1121,  0.3057, -0.1246],
          &#91; 0.3330,  0.3147, -0.0417],
          &#91;-0.0228,  0.7021, -0.0097],
          &#91; 0.0540,  0.2869,  0.1398],
          &#91;-0.0387,  0.5156, -0.0123],
          &#91;-0.0682,  0.3875,  0.1809],
          &#91;-0.1488,  0.4080, -0.0170],
          &#91;-0.3777,  0.6719, -0.4998],
          &#91;-0.0193,  0.1809, -0.5254]],

         &#91;&#91;-0.4082, -0.4016,  0.2546],
          &#91; 0.2925,  0.4536,  0.2051],
          &#91;-0.2620,  0.0322, -0.1362],
          &#91;-0.3171,  0.0995, -0.1333],
          &#91;-0.5464, -0.6416,  0.4722],
          &#91;-0.4858, -0.4067,  0.2091],
          &#91;-0.2115, -0.0323, -0.0919],
          &#91;-0.1204,  0.1034,  0.1997],
          &#91; 0.0822,  0.3242, -0.2644],
          &#91;-0.0407,  0.1741, -0.1674]]]], device='cuda:0', dtype=torch.float16)</code></pre>



<h4 class="wp-block-heading">PyTorch 的 FlashAttention 实现</h4>



<pre class="wp-block-code"><code># FlashAttention实现
with sdpa_kernel(SDPBackend.FLASH_ATTENTION):
    output_pytorch_flash = F.scaled_dot_product_attention(
        query, key, value,
        attn_mask=None,
        dropout_p=0.0,
        is_causal=False
    )

# Output: (batch_size, num_heads, seq_len, head_dim)
print(f"Shape:{output_pytorch_flash.shape}")
print(output_pytorch_flash)</code></pre>



<p>计算结果</p>



<pre class="wp-block-code"><code>Shape:torch.Size(&#91;1, 2, 10, 3])
tensor(&#91;&#91;&#91;&#91; 0.4641,  0.1558, -0.4280],
          &#91; 0.1121,  0.3059, -0.1246],
          &#91; 0.3330,  0.3147, -0.0417],
          &#91;-0.0228,  0.7021, -0.0098],
          &#91; 0.0540,  0.2866,  0.1398],
          &#91;-0.0386,  0.5156, -0.0124],
          &#91;-0.0682,  0.3875,  0.1809],
          &#91;-0.1487,  0.4080, -0.0170],
          &#91;-0.3777,  0.6724, -0.4998],
          &#91;-0.0193,  0.1810, -0.5254]],

         &#91;&#91;-0.4082, -0.4016,  0.2546],
          &#91; 0.2925,  0.4539,  0.2051],
          &#91;-0.2620,  0.0322, -0.1361],
          &#91;-0.3171,  0.0995, -0.1333],
          &#91;-0.5464, -0.6416,  0.4722],
          &#91;-0.4858, -0.4067,  0.2091],
          &#91;-0.2115, -0.0323, -0.0918],
          &#91;-0.1204,  0.1035,  0.1997],
          &#91; 0.0822,  0.3242, -0.2644],
          &#91;-0.0407,  0.1741, -0.1674]]]], device='cuda:0', dtype=torch.float16)</code></pre>



<h3 class="wp-block-heading">FlashAttention 计算结果</h3>



<pre class="wp-block-code"><code>Shape:torch.Size(&#91;1, 2, 10, 3])
tensor(&#91;&#91;&#91;&#91; 0.4641,  0.1556, -0.4280],
          &#91; 0.1121,  0.3057, -0.1247],
          &#91; 0.3330,  0.3147, -0.0420],
          &#91;-0.0229,  0.7017, -0.0099],
          &#91; 0.0542,  0.2866,  0.1396],
          &#91;-0.0385,  0.5156, -0.0123],
          &#91;-0.0681,  0.3870,  0.1810],
          &#91;-0.1488,  0.4084, -0.0168],
          &#91;-0.3765,  0.6719, -0.5000],
          &#91;-0.0194,  0.1809, -0.5254]],

         &#91;&#91;-0.4080, -0.4019,  0.2554],
          &#91; 0.2925,  0.4536,  0.2053],
          &#91;-0.2620,  0.0321, -0.1362],
          &#91;-0.3171,  0.0994, -0.1332],
          &#91;-0.5464, -0.6421,  0.4727],
          &#91;-0.4856, -0.4067,  0.2091],
          &#91;-0.2118, -0.0326, -0.0917],
          &#91;-0.1205,  0.1035,  0.1996],
          &#91; 0.0822,  0.3240, -0.2644],
          &#91;-0.0406,  0.1741, -0.1674]]]], device='cuda:0', dtype=torch.float16)</code></pre>



<p>经过对比可以发现计算结果与参考值一致，误差可接受。</p>



<p>加入 mask 的计算结果与自己实现的 Attention 计算的结果是一致的，而 <strong>scaled_dot_product_attention</strong> 的标准数学实现在引入 mask 之后结果并没发生改变，同时其 FlashAttention 实现并不支持 mask 为非空的计算。这一点暂不理解。</p>



<hr class="wp-block-separator has-alpha-channel-opacity is-style-default"/>



<h1 class="wp-block-heading">Todo</h1>



<ol class="wp-block-list">
<li>目前正在尝试实现cuda版本的FlashAttention</li>
</ol>



<p></p>
]]></content:encoded>
					
					<wfw:commentRss>https://gsfall.cn/archives/93/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>WordPress 环境搭建教程</title>
		<link>https://gsfall.cn/archives/67</link>
					<comments>https://gsfall.cn/archives/67#respond</comments>
		
		<dc:creator><![CDATA[GushanFall]]></dc:creator>
		<pubDate>Wed, 09 Apr 2025 07:48:39 +0000</pubDate>
				<category><![CDATA[WordPress]]></category>
		<category><![CDATA[已分类]]></category>
		<category><![CDATA[教程]]></category>
		<guid isPermaLink="false">https://gsfall.cn/?p=67</guid>

					<description><![CDATA[Ubuntu + PHP + Nginx + MySQL + Wordpress 的建站过程]]></description>
										<content:encoded><![CDATA[
<h1 class="wp-block-heading">为什么要写该教程</h1>



<p>本文是对自己安装 WordPress 过程的一个总结。在安装过程中，翻阅了很多资料、教程，但是没有完全满足个人要求的教程。有的是其他环境比如 Apache2 ，有的是 CentOS。</p>



<p>所以既然自己成功安装了 WordPress，就顺便记录一下自己的安装过程，给可能需要的人提供一点帮助。</p>



<p>搭建过程主要参考了 <a href="https://www.cnblogs.com/frank3215/p/wordpress-ubuntu.html">总结｜在Ubuntu 22.04上设立WordPress &#8211; frank3215 &#8211; 博客园</a>，是在该作者的基础上进行增改，也增加了一些操作解释。</p>



<h1 class="wp-block-heading">环境要求</h1>



<ol class="wp-block-list">
<li><code>Ubuntu</code></li>



<li><code>PHP 7.4</code> / <code>PHP 8.0</code> （据说 <code>PHP 8.1</code> 可能会产生错误，但没亲自试过）</li>



<li><code>Nginx</code></li>



<li><code>MySQL 8.0+</code></li>
</ol>



<p>如果是其他的环境，本文可能提供不了帮助。</p>



<h1 class="wp-block-heading">安装依赖</h1>



<p>开始之前，更新软件包索引</p>



<pre class="wp-block-code"><code>sudo apt-get update</code></pre>



<h2 class="wp-block-heading">Nginx</h2>



<pre class="wp-block-code"><code>sudo apt-get install nginx</code></pre>



<h2 class="wp-block-heading">MySQL</h2>



<pre class="wp-block-code"><code># 安装 mysql 及所需依赖
sudo apt-get install mysql-client-core-8.0 mysql-server-core-8.0 mysql-server mysql-client --fix-missing 
# 启动 mysql 服务
sudo service mysql start</code></pre>



<h2 class="wp-block-heading">PHP</h2>



<p>添加第三方软件源以安装 PHP</p>



<pre class="wp-block-code"><code>sudo apt install software-properties-common ca-certificates lsb-release apt-transport-https
LC_ALL=C.UTF-8 sudo add-apt-repository ppa:ondrej/php</code></pre>



<p>更新软件源</p>



<pre class="wp-block-code"><code>sudo apt update</code></pre>



<p>安装 <code>php8.0</code></p>



<pre class="wp-block-code"><code>sudo apt-get install php8.0 php8.0-cli php8.0-fpm php8.0-mysql php8.0-opcache php8.0-mbstring php8.0-xml php8.0-gd php8.0-curl php8.0-cgi</code></pre>



<p>如果要安装 <code>php7.4</code> ，请参考 <a href="https://www.cnblogs.com/frank3215/p/wordpress-ubuntu.html">总结｜在Ubuntu 22.04上设立WordPress &#8211; frank3215 &#8211; 博客园</a></p>



<p>设置开机自启动</p>



<pre class="wp-block-code"><code>sudo systemctl enable php8.0-fpm</code></pre>



<p>下面的命令可以查看运行状态</p>



<pre class="wp-block-code"><code>sudo systemctl status php8.0-fpm</code></pre>



<p>注意：<code>/etc/php/8.0/fpm/php.ini</code> 中可修改上传文件的大小限制</p>



<h1 class="wp-block-heading">安装并配置 WordPress</h1>



<h2 class="wp-block-heading">下载 WordPress</h2>



<p>下载并解压最新版的 WordPress</p>



<pre class="wp-block-code"><code>wget https://wordpress.org/latest.zip 
unzip latest.zip

# 如果未安装 unzip 请提前安装
sudo apt install unzip</code></pre>



<h2 class="wp-block-heading">用 MySQL 建立数据库</h2>



<p>以管理员登录数据库</p>



<pre class="wp-block-code"><code>sudo mysql -u adminusername -p</code></pre>



<p>创建数据库 <code>wordpress</code></p>



<pre class="wp-block-code"><code>CREATE DATABASE wordpress;</code></pre>



<p>创建用户和设置密码（ <code>password</code> 可以自己任意设置），并给予权限</p>



<pre class="wp-block-code"><code>CREATE USER 'wordpress'@'localhost' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON * . * TO 'wordpress'@'localhost';
FLUSH PRIVILEGES;
EXIT;</code></pre>



<h2 class="wp-block-heading">修改 <code>wp-config.php</code></h2>



<p>进入 <code>wordpress</code> 文件夹，复制 <code>wp-config-sample.php</code> 为 <code>wp-config.php</code></p>



<pre class="wp-block-code"><code>cd wordpress 
cp wp-config-sample.php wp-config.php</code></pre>



<p>打开 <code>wp-config.php</code> ，修改以下内容</p>



<p>首先修改数据库设置， <code>password</code> 为前面设置的密码</p>



<pre class="wp-block-code"><code>// ** Database settings - You can get this info from your web host ** //
/** The name of the database for WordPress */
define( 'DB_NAME', 'wordpress' );

/** Database username */
define( 'DB_USER', 'wordpress' );

/** Database password */
define( 'DB_PASSWORD', 'password' );

/** Database hostname */
define( 'DB_HOST', 'localhost' );</code></pre>



<p>然后找到以下内容，填入 <a href="https://api.wordpress.org/secret-key/1.1/salt/">生成器</a> 中随机生成的值</p>



<pre class="wp-block-code"><code>/** Authentication Unique Keys and Salts. */
define( 'AUTH_KEY',         '' );
define( 'SECURE_AUTH_KEY',  '' );
define( 'LOGGED_IN_KEY',    '' );
define( 'NONCE_KEY',        '' );
define( 'AUTH_SALT',        '');
define( 'SECURE_AUTH_SALT', '' );
define( 'LOGGED_IN_SALT',   '' );
define( 'NONCE_SALT',       '' );</code></pre>



<p>最后回到上一级目录，将整个 <code>wordpress</code> 移动到 <code>/var/www/html/</code> 中，这是 Nginx 服务器的默认文件目录</p>



<pre class="wp-block-code"><code>cd ..
sudo mv wordpress /var/www/html/wordpress</code></pre>



<p>设置正确的文件权限和所有权，以确保</p>



<ul class="wp-block-list">
<li>Web 服务器（<code>www-data</code> 用户）能够读取和写入必要的文件。</li>



<li>其他用户只能读取文件和目录，而不能修改它们。</li>
</ul>



<pre class="wp-block-code"><code>chown -R www-data:www-data /var/www/html
sudo find /var/www/html/wordpress -type d -exec chmod 755 {} \;
sudo find /var/www/html/wordpress -type f -exec chmod 644 {} \;</code></pre>



<h2 class="wp-block-heading">配置 Nginx</h2>



<p>修改 <code>/etc/nginx/sites-available/wordpress</code>，没有则自己创建，其中 <code>example.com</code> 改为自己的域名</p>



<pre class="wp-block-code"><code># WordPress 站点配置文件
server {
    # 监听 HTTP 请求
    listen 80;
    listen &#91;::]:80;

    # 定义网站的域名
    server_name example.com;

    # 设置网站的根目录
    root /var/www/html/wordpress;

    # 定义默认的索引文件
    index index.php index.html index.htm index.nginx-debian.html;

    # 处理网站根路径请求
    location / {
        # 如果请求的文件或目录不存在，则转发到 index.php
        try_files $uri $uri/ /index.php?$args;
    }

    # 处理 WordPress 站点地图
    location ~* /wp-sitemap.*\.xml {
        # 如果站点地图文件不存在，则转发到 index.php
        try_files $uri $uri/ /index.php$is_args$args;
    }

    # 设置上传文件大小限制
    client_max_body_size 100M;

    # 处理 PHP 文件
    location ~ \.php$ {
        # 将 PHP 文件的处理请求转发到 PHP-FPM 服务
        fastcgi_pass unix:/run/php/php8.0-fpm.sock;

        # 设置脚本文件路径
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

        # 包含 FastCGI 参数文件
        include fastcgi_params;
        include snippets/fastcgi-php.conf;

        # 设置 FastCGI 缓冲区大小
        fastcgi_buffer_size 128k;
        fastcgi_buffers 4 128k;

        # 拦截并处理 PHP 错误
        fastcgi_intercept_errors on;
    }

    # 启用 Gzip 压缩
    gzip on;
    gzip_comp_level 6;  # 压缩级别
    gzip_min_length 1000;  # 最小压缩长度
    gzip_proxied any;  # 允许对代理请求进行压缩
    gzip_disable "msie6";  # 禁止对 IE6 浏览器进行压缩

    # 定义需要压缩的 MIME 类型
    gzip_types
        application/atom+xml
        application/geo+json
        application/javascript
        application/x-javascript
        application/json
        application/ld+json
        application/manifest+json
        application/rdf+xml
        application/rss+xml
        application/xhtml+xml
        application/xml
        font/eot
        font/otf
        font/ttf
        image/svg+xml
        text/css
        text/javascript
        text/plain
        text/xml;

    # 处理静态资源（图片、音频、视频等）
    location ~* \.(?:css(\.map)?|js(\.map)?|jpe?g|png|gif|ico|cur|heic|webp|tiff?|mp3|m4a|aac|ogg|midi?|wav|mp4|mov|webm|mpe?g|avi|ogv|flv|wmv)$ {
        # 设置缓存过期时间为 90 天
        expires 90d;

        # 关闭访问日志记录
        access_log off;
    }

    # 处理字体和 SVG 文件
    location ~* \.(?:svgz?|ttf|ttc|otf|eot|woff2?)$ {
        # 允许跨域请求这些资源
        add_header Access-Control-Allow-Origin "*";

        # 设置缓存过期时间为 90 天
        expires 90d;

        # 关闭访问日志记录
        access_log off;
    }

    # 保护敏感文件（如 .htaccess）
    location ~ /\.ht {
        # 禁止访问这些文件
        deny all;

        # 关闭日志记录
        access_log off;
        log_not_found off;
    }
}</code></pre>



<p>最后在 Nginx Web 服务器中启动 WordPress 站点，并检查配置文件是否正确，最后重启 Nginx 服务</p>



<pre class="wp-block-code"><code>sudo ln -s /etc/nginx/sites-available/wordpress /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx</code></pre>



<p>现在应该可以在 <code>example.com</code> 上看见 WordPress 页面</p>



<p>如果没有域名，则可通过 80 端口访问，在此之前，先将服务器上的 80 端口转发到本地</p>



<p>WordPress 安装界面如下</p>


<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><img loading="lazy" decoding="async" width="415" height="584" src="https://gsfall.cn/wp-content/uploads/2025/04/wordpress-install.png" alt="wordpress 安装界面" class="wp-image-78" style="width:424px;height:auto" srcset="https://gsfall.cn/wp-content/uploads/2025/04/wordpress-install.png 415w, https://gsfall.cn/wp-content/uploads/2025/04/wordpress-install-213x300.png 213w" sizes="auto, (max-width: 415px) 100vw, 415px" /></figure>
</div>


<h1 class="wp-block-heading">更简单的方法</h1>



<p>如果在阿里云上购买的云服务器，可以直接在镜像市场中寻找对应的 WordPress 镜像，即开即用。</p>



<p></p>
]]></content:encoded>
					
					<wfw:commentRss>https://gsfall.cn/archives/67/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
